# -*- coding: UTF-8 -*-


# Expression
# By: Frederic Peters <fpeters@entrouvert.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


"""Directories Module"""


import errno
import os

import dataholders
import documents
import elements
import environs
import faults
import filesystems
import logs
import namespaces
import stations
import things


class Directory(things.Thing):
    pass


class DirectoryHolder(dataholders.DataHolder):
    _itemLocations = None
    _items = None
    _itemsListDocument = None
    defaultFileNameExtension = None
    isUriDirectory = True
    mimeType = "inode/directory"

    def destroy(self):
        parent = self.getParent()
        if parent is not None:
            parent.itemDestroyed(self)
        configFileSystemPath = os.path.join(self.getContainedFileSystemPath(), "_config.xml")
        self.containedFileSystem.remove(configFileSystemPath)
        # FIXME: Handle non empty directories.
        self.fileSystem.rmdir(self.getFileSystemPath())

    def doHttpDelete(self):
        import webdav
        content = environs.getVar("submission").readFile()
        if content is not None:
            logs.debug(content)
        multistatus = webdav.Multistatus.newInTemporaryDocument()
        if not self.doHttpDeleteItems(multistatus):
            data = multistatus.getDocument().serialize()
            logs.debug("\nDELETE response =\n%s" % data)
            return environs.getVar("httpRequestHandler").outputData(
                data, contentLocation = None, mimeType = "text/xml", successCode = 207)
        fileSystemPath = self.getFileSystemPath()
        try:
            self.fileSystem.rmtree(fileSystemPath)
        except OSError, error:
            if error.errno != errno.EPERM:
                raise
            return environs.getVar("httpRequestHandler").outputErrorAccessForbidden(
                self.getUriAbsolutePath())
        environs.getVar("httpRequestHandler").outputSuccessNoContent()

    def doHttpDelete1(self, multistatus):
        if not self.doHttpDeleteItems(multistatus):
            return False
        fileSystemPath = self.getFileSystemPath()
        try:
            self.fileSystem.rmtree(fileSystemPath)
        except OSError, error:
            if error.errno != errno.EPERM:
                raise
            multistatusNode = multistatus.node
            responseNode = multistatusNode.newTextChild(None, "response", None)
            # The href URI is quoted, because Nautilus requires that the returned href is the
            # same as the URL in the PROPFIND command. Otherwise it can not open "untitled
            # folder", because it expects "untitled%20folder".
            responseNode.newTextChild(None, "href", self.getAbsoluteUri(quote = True))
            responseNode.newTextChild(None, "status", "HTTP/1.1 403 Access Forbidden")
            return False
        return True

    def doHttpDeleteItems(self, multistatus):
        result = True
        for item in self.iterItems():
            if not item.isAccessAuthorized("DELETE"):
                multistatusNode = multistatus.node
                responseNode = multistatusNode.newTextChild(None, "response", None)
                # The href URI is quoted, because Nautilus requires that the returned href is the
                # same as the URL in the PROPFIND command. Otherwise it can not open "untitled
                # folder", because it expects "untitled%20folder".
                responseNode.newTextChild(None, "href", item.getAbsoluteUri(quote = True))
                responseNode.newTextChild(None, "status", "HTTP/1.1 401 Access Unauthorized")
            elif not item.doHttpDelete1(multistatus):
                result = False
        return result

    def doHttpGet(self):
        if self.getInRawMode():
            # In raw mode, we display the content of a directory as an HTML page listing all items.
            # It allows non WebDAV compliant clients (ie Mozilla, ...) to be still used to
            # administer the web site.
            document = self.getItemsListDocument()
            return document.styled()
        else:
            modeNames = self.getConfigString("yep:defaultMode", default = "index").lower()
            for modeName in modeNames.split():
                if modeName == "index":
                    try:
                        return self.walk(["index"], environs.getVar("httpCommand"))
                    except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized):
                        pass
                elif modeName == "list":
                    try:
                        return self.list()
                    except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), \
                               fault:
                        logs.exception("Ignoring fault %s in items list" % fault)
                elif modeName == "view":
                    return super(DirectoryHolder, self).doHttpGet()
                else:
                    logs.info('Ignoring unknown mode "%s" for directory at "%s".' % (
                        modeName, self.getAbsolutePath()))
            raise faults.PathForbidden("")

    def doHttpMkcol(self, collectionName = None):
        if collectionName is None:
            return super(DirectoryHolder, self).doHttpMkcol()
        content = environs.getVar("submission").readFile()
        if content is not None:
            logs.debug(content)
        self.containedFileSystem.mkdir(os.path.join(
            self.getContainedFileSystemPath(), collectionName))
        environs.getVar("httpRequestHandler").outputSuccessCreated(collectionName)

    def doHttpPut(self, itemName = None):
        # FIXME: To complete.
        # See RFC 2616 <http://www.ietf.org/rfc/rfc2616.txt>
        # and the full thread at
        # <http://www.mail-archive.com/www-talk@w3.org/msg00699.html>
        # to know how to handle HTTP PUT correctly.

        httpRequestHandler = environs.getVar("httpRequestHandler")
        if itemName is None:
            return httpRequestHandler.outputErrorMethodNotAllowed("Not a document")

        itemNameCore, itemNameExtension = os.path.splitext(itemName)
        if itemNameExtension in dataholders.mimeTypes:
            mimeType = dataholders.mimeTypes[itemNameExtension]
        else:
            logs.debug('Unknown file name extension = "%s"' % itemNameExtension)
            mimeType = "application/octet-stream"

        # FIXME: We should check whether the save can be done before sending
        # a "continue" response.
        expect = httpRequestHandler.headers.get("Expect", "")
        if expect.lower() == "100-continue":
            httpRequestHandler.outputInformationContinue()
        newHolder = dataholders.StaticDataHolder(
            pathFragment = itemName, previous = self, uriPathFragment = itemName,
            mimeType = mimeType)
        submission = environs.getVar("submission")
        isCreated = newHolder.saveDataFile(submission.file, submission.length)
        if isCreated:
            httpRequestHandler.outputSuccessCreated(self.getUriAbsolutePath(), station = newHolder)
        else:
            httpRequestHandler.outputSuccessNoContent()

    def getDirectoryAbsolutePath(self):
        return self.getAbsolutePath()

    def getItem(self, itemLocalId, withoutUri = False):
        if self._items is None or itemLocalId not in self._items:
            itemLocations = self.getItemLocations()
            if itemLocalId not in itemLocations:
                return None
            itemLocation = itemLocations[itemLocalId]
            itemLocationCore, itemLocationExtension = os.path.splitext(itemLocation)
            mimeType = None
            if itemLocationExtension in dataholders.mimeTypes:
                mimeType = dataholders.mimeTypes[itemLocationExtension]
            elif itemLocationExtension != "":
                itemLocationCore, itemLocationExtension = itemLocation, ''
                logs.debug("""\
Unknown file name extension = "%s", length = %d, for file "%s" at "%s".\
""" % (
                    itemLocationExtension, len(itemLocationExtension),
                    itemLocation, self.getAbsolutePath()))
                #mimeType = "application/octet-stream"
            if withoutUri:
                uriPathFragment = None
            else:
                uriPathFragment = itemLocalId
            if itemLocation == itemLocalId:
                item = dataholders.DataHolder(
                    pathFragment = itemLocalId, previous = self,
                    uriPathFragment = uriPathFragment, mimeType = mimeType)
            else:
                # ItemLocation is always an absolute path.
                item = dataholders.DataHolder(
                    pathFragment = itemLocation, previous = self,
                    uriPathFragment = uriPathFragment, mimeType = mimeType, isRootElder = True,
                    containedFileSystem = filesystems.PartialFileSystem(itemLocation))
            if self._items is None:
                self._items = {}
            self._items[itemLocalId] = item
        return self._items[itemLocalId]

    def getItemLocalId(self, itemName):
        itemLocalIds = self.getItemLocations().keys()
        if itemName in itemLocalIds:
            itemLocalId = itemName
        else:
            filenameMatchs = [x for x in itemLocalIds if os.path.splitext(x)[0] == itemName]
            if itemName + ".xml" in filenameMatchs:
                itemLocalId = itemName + ".xml"
            elif filenameMatchs:
                itemLocalId = filenameMatchs[0]
            else:
                itemLocalId = None
        return itemLocalId

    def getItemLocations(self):
        if self._itemLocations is None:
            # Item locations that begins with "/" are absolute paths (ie not base relative).
            if self.previous is None:
                # Root directory of a virtual host.
                itemLocations = environs.getVar("virtualHost").getItemLocations()
            else:
                itemLocations = {}
            if self.fileSystem.exists(self.getFileSystemPath()):
                try:
                    fileNames = self.containedFileSystem.listdir(self.getContainedFileSystemPath())
                except OSError, error:
                    if error.errno == errno.EACCES:
                        # Permission denied
                        logs.debug("Permission denied: listdir(%s)" % self.getAbsolutePath())
                        fileNames = []
                    else:
                        raise
                for fileName in fileNames:
                    if fileName[0] in ".#" or fileName[-1] == "~":
                        continue
                    if fileName in ("_config.xml", "_meta.xml", "CVS"):
                        continue
                    fileNameCore, fileNameExtension = os.path.splitext(fileName)
                    if fileNameExtension in (".pyc", ".pyo"):
                        continue
                    if fileName.endswith(".config.xml") or fileName.endswith(".meta.xml"):
                        continue
                    itemLocations[fileName] = fileName
            self._itemLocations = itemLocations
        return self._itemLocations

    def getItemsListDocument(self):
        if self._itemsListDocument is None:
            document = documents.newTemporaryDocument(previous = self)
            directoryNode = document.node.newTextChild(None, "directory", None)
            namespace = directoryNode.newNs(namespaces.yep.uri, None)
            directoryNode.setNs(namespace)
            itemLocations = self.getItemLocations()
            itemLocalIds = itemLocations.keys()
            itemLocalIds.sort()
            itemsNode = directoryNode.newTextChild(None, "items", None)
            for itemLocalId in itemLocalIds:
                itemNode = itemsNode.newTextChild(None, "item", None)
                itemNode.newProp("name", itemLocalId)
                itemLocation = itemLocations[itemLocalId]
                if itemLocation != itemLocalId:
                    itemNode.newProp("src", itemLocation)
            self._itemsListDocument = document
        return self._itemsListDocument

    def getSimplestSourceUrl(self):
        # A directory has no source.
        return None

    def getSimplestStyledUrl(self):
        # A directory is always displayed as styled.
        return self.getUri()

    def getUriDirectoryAbsolutePath(self):
        return self.getUriAbsolutePath()

    def getUriDirectoryInternPath(self):
        return self.getUriInternPath()

    def getWebDavCollectionItems(self):
        itemLocalIds = self.getItemLocations().keys()
        itemLocalIds.sort()
        return [self.getItem(itemLocalId) for itemLocalId in itemLocalIds]

    def getWebDavContentLength(self):
        return None
        
    def isWebDavCollection(self):
        return True

    def itemDestroyed(self, item):
        """Called before an item (hierarchical) is destroyed."""

        if self._items is None:
            return
        itemLocalId = item.localId
        if itemLocalId not in self._items:
            return
        del self._items[itemLocalId]
        if not self._items:
            del self._items

    def itemRenamed(self, item, itemOldLocalId):
        """Called after an item (hierarchical) is renamed."""

        if self._items is None or itemOldLocalId not in self._items:
            return
        del self._items[itemOldLocalId]
        self._items[item.localId] = item

    def iterItems(self, withoutUri = False, recursive = False):
        itemLocations = self.getItemLocations()
        itemLocalIds = itemLocations.keys()
        itemLocalIds.sort()
        for itemLocalId in itemLocalIds:
            item = self.getItem(itemLocalId, withoutUri = withoutUri)
            yield item
            if recursive and item.isWebDavCollection():
                for subItem in item.iterItems(withoutUri = withoutUri, recursive = recursive):
                    yield subItem

    def list(self):
        """Handles HTTP GET."""
        document = self.getItemsListDocument()
        return document.walk([], environs.getVar("httpCommand"), environs.getVar("instruction"))

    def newItemLocalId(self):
        # FIXME: Compute a real localId.
        import whrandom
        randomGenerator = whrandom.whrandom()
        return str(randomGenerator.uniform(0.1, 1))[2:]

    def outputHttpSource(self):
        raise faults.PathForbidden("")

##     def save(self):
##         fileSystem = self.fileSystem
##         fileSystemPath = self.getFileSystemPath()
##         if not fileSystem.exists(fileSystemPath):
##             fileSystem.mkdir(fileSystemPath)
##         indexFileSystemPath = os.path.join(
##             self.getContainedFileSystemPath(), "index.xml")
##         publicName = self.publicName
##         localId = self.localId
##         if publicName and self.defaultFileNameExtension \
##                 and "." not in publicName:
##             publicName += self.defaultFileNameExtension
##         if publicName and publicName != localId:
##             isRenamed = True
##             oldLocalId = localId
##             oldFileSystemPath = fileSystemPath
##             localId = publicName
##             self.pathFragment = localId
##             if self.uriPathFragment == oldLocalId:
##                 self.uriPathFragment = localId
##         else:
##             isRenamed = False
##         del self.publicName
##         if itemsNodes:
##             # Don't serialize items if they were generated automaticaly.
##             itemsNode = itemsNodes[0]
##             itemsNode.unlinkNode()
##             data = self.serialize()
##             self.getRootNode().addChild(itemsNode)
##         else:
##             data = self.serialize()
##         self.publicName = publicName
##         if isRenamed:
##             indexFileSystemPath = os.path.join(
##                 self.getContainedFileSystemPath(), "index.xml")
##             fileSystem.rename(oldFileSystemPath, self.getFileSystemPath())
##         dataFile = self.containedFileSystem.open(indexFileSystemPath, "wb")
##         dataFile.write(data)
##         dataFile.close()
##         if isRenamed:
##             parent = self.getParent()
##             if parent is not None:
##                 parent.itemRenamed(self, oldLocalId)

    def refresh(self):
        """ resets cache, forcing the directory content to be read again.

        """
        self._itemLocations = None
        self._items = None
        self._itemsListDocument = None

    def styled(self):
        """Handles HTTP GET."""
        command = environs.getVar("httpCommand")
        if command != "GET":
            raise faults.PathNotFound("")
        modeNames = self.getConfigString("yep:defaultMode", default = "index").lower()
        for modeName in modeNames.split():
            if modeName == "index":
                try:
                    return self.walk(["index", "styled"], environs.getVar("httpCommand"))
                except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized):
                    pass
            elif modeName == "list":
                document = self.getItemsListDocument()
                try:
                    return document.walk(
                        ["styled"], environs.getVar("httpCommand"), environs.getVar("instruction"))
                except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), \
                           fault:
                    logs.exception("Ignoring fault %s in items list" % fault)
            elif modeName == "view":
                return super(DirectoryHolder, self).styled()
            else:
                logs.info('Ignoring unknown mode "%s" for directory at "%s".' % (
                    modeName, self.getAbsolutePath()))
        raise faults.PathForbidden("styled")
        
    def walkToAction(self, uriPathFragments, command = None, instruction = None):
        actionName = uriPathFragments[0]
        if actionName.startswith("new-"):
            namespaceName, elementName = actionName[len("new-"):].split("-", 1)
            namespaceUri = namespaces.getUri(namespaceName)
            elementFeature = self.getElementFeature(namespaceUri, elementName)
            # If no element feature found, the action is not possible.
            if elementFeature is None:
                return self, uriPathFragments
            # Return the station that will create the element.
            constructorStation = stations.HolderConstructorStation(
                actionName, elementFeature, previous = self)
            return constructorStation, uriPathFragments[1:]
        return super(DirectoryHolder, self).walkToAction(uriPathFragments, command, instruction)

    def walkToItem(self, uriPathFragments, command = None, instruction = None):
        itemName = uriPathFragments[0]
        itemLocalId = self.getItemLocalId(itemName)
        if itemLocalId is not None:
            item = self.getItem(itemLocalId)
            if item is not None:
                return item, uriPathFragments[1:]
        return super(DirectoryHolder, self).walkToItem(uriPathFragments, command, instruction)


elements.registerElement(
    namespaces.yep.uri, "directory", Directory,
    "http://www.entrouvert.org/expression/schemas/Directory.xsd",
    "http://www.entrouvert.org/expression/descriptions/Directory.xml", DirectoryHolder)
