#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
#
# sebastien.martini@gmail.com
#
# 07/20/2006 - MIT License
#
import os
import sys
import stat
import errno
import time
import itertools
import logging
import StringIO # needed by class File
import fuse


__metaclass__ = type  # Use new-style classes by default


# debug mode, turn on for extra messages
# output in  '/tmp/vtagfs.log'
DEBUG = False

if DEBUG:
    LOGPATH = '/tmp/'
    # setup logging functionality
    logging.basicConfig(level=logging.DEBUG,
                        format='%(asctime)s %(levelname)s %(message)s',
                        filename=os.path.join(LOGPATH, 'vtagfs.log'),
                        filemode='w')

if DEBUG:
    # outputing interpreter's errors in a file
    FO = file(os.path.join(LOGPATH, 'vtagfs-errors.log'), 'w')
    sys.stderr = FO

FUSED = None  # fuse.Fuse instance, this decl. scope is required


## inodes

class INode:
    """
    inode _abstract_ base class, subclass it and redifine .get_stat()
    """
    def __init__(self, inv_tag_index, name=''):
        self._inv_tag_index = inv_tag_index  # {'tag': [inode1, inode2,...],...]
        self._name = name
        self._tags = []  # [tag1, tag2,...]
        self._stat = self.do_stat()

    def add_tag(self, tag):
        if tag in self._tags:
            return
        # append in tag list
        self._tags.append(tag)
        # insert in an inverted index
        if self._inv_tag_index.has_key(tag):
            self._inv_tag_index[tag].append(self)
        else:
            self._inv_tag_index[tag] = [self]

    def remove_tag(self, tag):
        if not tag in self._tags:
            return
        # remove tag from list
        self._tags.remove(tag)
        # remove tag from inverted index
        if self._inv_tag_index.has_key(tag):
            self._inv_tag_index[tag].remove(self)

    def do_stat(self):
        assert not "must be overriden"

    def get_stat(self, attr=''):
        self.update_stat()
        if not attr:
            return self._stat
        return getattr(self._stat, attr, None)

    def update_stat(self, name='', value=''):
        if name and hasattr(self._stat, name):
            setattr(self._stat, name, value)


class SLINode(INode):
    """
    inode for symlinks.
    """
    def __init__(self, inv_tag_index, name, target):
        self._target = target
        INode.__init__(self, inv_tag_index, name)

    def do_stat(self):
        st_ = FileStat()
        st_.st_mode = stat.S_IFLNK | 0777
        st_.st_size = len(self._target)
        return st_


class FINode(INode):
    """
    inode for files. ram files.
    """
    def __init__(self, inv_tag_index, name, mode, dev):
        self.mode_ = mode
        self.dev_ = dev
        self._data = ''
        INode.__init__(self, inv_tag_index, name)

    def do_stat(self):
        st_ = FileStat()
        # fixme: correct?: file_type | rights
        st_.st_mode = stat.S_IFMT(self.mode_) | 0644
        st_.st_dev = self.dev_
        st_.st_size
        del self.mode_, self.dev_
        return st_

    def update_stat(self, name='', value='', all=True):
        INode.update_stat(self, name, value)
        if all:
            self._stat.st_size = len(self._data)


## Stat

class Stat(fuse.Stat):
    """
    abstract subclass of stub class: fuse.Stat
    """
    def __init__(self):
        self.st_uid = fuse.FuseGetContext()['uid']
        self.st_gid = fuse.FuseGetContext()['gid']
        self.st_atime = self.st_mtime = self.st_ctime = int(time.time())

    def __str__(self):
        # attributes to provide
        return "(%d, %d, %d, %d, %d, %d, %d, %d, %d, %d)" % \
               (self.st_mode, self.st_ino, self.st_dev, self.st_nlink,
                self.st_uid, self.st_gid, self.st_size, self.st_atime,
                self.st_mtime, self.st_ctime)

class DirStat(Stat):
    def __init__(self):
        Stat.__init__(self)
        self.st_mode = stat.S_IFDIR | 0755
        self.st_nlink = 2
        self.st_ino = 0
        self.st_dev = 0
        self.st_size = 0

class FileStat(Stat):
    def __init__(self):
        Stat.__init__(self)
        self.st_ino = 0
        self.st_dev = 0
        self.st_nlink = 1
        self.st_size = 0


## Direntry

class Direntry2(fuse.Direntry):
    """
    subclass fuse.Direntry
    """
    def __init__(self, name, type=0):
        fuse.Direntry.__init__(self, name)
        self.type = type

    def __str__(self):
        return "(%d, %d, %d, %d)" % \
               (self.name, self.offset, self.type, self.ino)


## utils

def all(s):
    for x in s:
        if not x:
            return False
    return True

def intersection(s1, s2):
    """
    make requirements that s1's elements are hashable.
    """
    d = {}
    for x in s1:
      d[x] = 1
    return [x for x in s2 if d.has_key(x)]

def reduce1(fun, s):
    """
    enhance reduce() by working even with generators.
    """
    ite = iter(s)
    acc = ite.next()
    for i in ite:
        acc = fun(acc, i)
    return acc

def nreduce(fun, s, n):
    """
    FIXME: actually useless, never instanciated

    If n == 2 it can work on a left associative bin op
    sequences like:
    1 + 2 + 3 + 4 == fun(+,fun(+,fun(+, 1, 2), 3), 4)
    Moreover, s could be a generator.
    """
    ite = iter(s)
    acc = ite.next()
    accarg = []
    for c, e in enumerate(ite):
        accarg.append(e)
        if (c + 1) % (n - 1) == 0:
            acc = fun(acc, *tuple(accarg))
            accarg = []
    if len(accarg):
        return None
    return acc

def foreach(fun, s):
    """
    apply fun to every elements of s
    """
    for e in s:
        fun(e)


class BinOp:
    """
    FIXME: actually useless, never instanciated
    """
    def __init__(self):
        self._ops = {'i': self.intersection,
                     'u': self.union}

    def has_op(self, op):
        if self._ops.has_key(op):
            return True
        return False

    def __call__(self, s1, op, s2):
        if self.has_op(op):
            return self._ops[op](s1, s2)
        return None

    def intersection(self, s1, s2):
        """
        make requirements that s1's elements are hashable.
        """
        d = {}
        for x in s1:
            d[x] = 1
        return [x for x in s2 if d.has_key(x)]

    def union(self, s1, s2):
        """
        make requirements that seq's elements are hashable.
        """
        d, ret = {}, []
        for x in itertools.chain(s1, s2):
            if not d.has_key(x):
                d[x] = 1
                ret.append(x)
        return x


class VTagFS(fuse.Fuse):
    """
    vtag fs
    """
    # extanded attributes namespace
    XATTR_NAMESPACE = 'system.vtagfs.'

    def __init__(self, *args, **kw):
        fuse.Fuse.__init__(self, *args, **kw)
        self._inodes = []
        self._inv_tag_index = {}


    # stat

    def getattr(self, path):
        if DEBUG:
            logging.debug('getattr: path: %s' % path)

        if path == '/':
            return DirStat()

        c_ = self._extract_from_path(path)

        # fixme: debug
        if DEBUG:
            logging.debug('getattr: type: %s' % c_)
        if c_ is None:
            return -errno.ENOENT
        elif isinstance(c_, INode):
            return c_.get_stat()
        else:
            return DirStat()


    # dirs aka tags

    def readdir(self, path, offset):
        if DEBUG:
            logging.debug('readdir: path: %s' % path)

        dots, entries = [Direntry2(name, stat.S_IFDIR) \
                         for name in ('.', '..')], []

        if path == '/':
            entries = [Direntry2(tag, stat.S_IFDIR) \
                       for tag in self._inv_tag_index.iterkeys()]
        else:
            entries = self._extract_from_path(path)
            if entries is None or isinstance(entries, INode):
                yield -errno.ENOENT

        for direntry in itertools.chain(dots, entries):
            yield direntry

    def _is_valid_tag(self, name):
        if ' ' in name:
            return False
        #if self._binops.has_op(name):
        #    return False
        return True

    def mkdir(self, path, mode):
        if DEBUG:
            logging.debug('mkdir: path: %s' % path)
        spath =  self._split_path(path)

        for tag in spath:
            if self._is_valid_tag(tag) and \
                   (not self._inv_tag_index.has_key(tag)):
                self._inv_tag_index[tag] = []

    def rmdir(self, path):
        if DEBUG:
            logging.debug('rmdir: path: %s' % path)
        spath =  self._split_path(path)

        for tag in spath:
            if self._inv_tag_index.has_key(tag) and \
                   len(self._inv_tag_index[tag]) == 0:
                del self._inv_tag_index[tag]


    ## paths manipulations

    def _split_path(self, path):
        """
        split the path and return a list of chunks.
        """
        s_ = path.split(os.sep)
        return [i for i in s_ if i != '']

    def _get_inode(self, name):
        for inode_ in self._inodes:
            if inode_._name == name:
                return inode_
        return None

    def _get_direntry_list(self, inodes):
        for inode_ in inodes:
            yield Direntry2(inode_._name,
                            stat.S_IFMT(inode_.get_stat('st_mode')))

    def _get_intersection(self, tags):
        try:
           i_ = reduce1(intersection,
                        (self._inv_tag_index[tag_] for tag_ in tags))
        except KeyError:
            return None
        return i_

    def _extract_from_path(self, path):
        spath_ = self._split_path(path)

        if not len(spath_):
            return None

        if len(spath_) == 1:
            if self._inv_tag_index.has_key(spath_[0]):
                return self._get_direntry_list(self._inv_tag_index[spath_[0]])
            else:
                return self._get_inode(spath_[0])

        inode_ = self._get_inode(spath_[-1])
        valid_tag_ = self._inv_tag_index.has_key(spath_[-1])
        if inode_ is None and not valid_tag_:
            return None
        inodes_ = self._get_intersection(spath_[:-1])
        if inodes_ is None:
            return None
        if not(inode_ is None) and (inode_ in inodes_):
            return inode_
        elif valid_tag_:
            inodes_ = intersection(inodes_, self._inv_tag_index[spath_[-1]])
            return self._get_direntry_list(inodes_)
        else:
            return None


    ## symlinks

    def symlink(self, src, dst):
        if DEBUG:
            logging.debug('symlink: src:%s :dst:%s' % (src,dst))

        r_ = self._split_path(dst)
        if len(r_) < 2:
            # each sl must be associated at least to one tag
            return -errno.EINVAL

        # create inode if it not exist yet
        inode_ = self._get_inode(r_[-1])
        if inode_ is None:
            inode_ = SLINode(self._inv_tag_index, r_[-1], src)
            self._inodes.append(inode_)

        # register tags
        #inode_.add_tag('all')  # fixme: to remove?
        foreach(inode_.add_tag, r_[:-1])


    def readlink(self, path):
        if DEBUG:
            logging.debug('readlink: path: %s' % path)

        # we dont call directely get_inode() because we must
        # know we have the right to read the link given its tags.
        inode_ = self._extract_from_path(path)
        if inode_ is None or not isinstance(inode_, SLINode):
            return -errno.ENOENT
        return inode_._target


    ## files manipulations

    def utime(self, path, times):
        """
        update atime (access) and mtime (modif.)
        """
        if DEBUG:
            logging.debug('utime: path: %s :times: %s' % (path, times))

        # see comment readlink
        inode_ = self._extract_from_path(path)
        if inode_ is None or (not isinstance(inode_, INode)):
            return -errno.ENOENT
        inode_._stat.st_atime, inode_._stat.st_mtime = times

    def truncate(self, path, len):
        """
        truncate the _inode_

        note: I don't know why the method is called instead of
              ftruncate but sometimes it does.
        """
        if DEBUG:
            logging.debug('truncate: path: %s :len: %d' % (path, len))

        inode_ = self._extract_from_path(path)
        if inode_ is None or (not isinstance(inode_, FINode)):
            return -errno.ENOENT
        sio_ = StringIO.StringIO(inode_._data)
        sio_.truncate(len)
        inode_._data = sio_.getvalue()
        sio_.close()

    def mknod(self, path, mode, dev):
        """
        called on old kernel/fuse.
        """
        if DEBUG:
            logging.debug('mknod: path: %s :mode: %s :dev: %s' % \
                          (path, mode, dev))

        # We do not call sth like os.mknod because we dont
        # write anything back on disk, in reality everything
        # stay in memory.
        # Needless to say, we must be careful to not write big
        # length of data.

        r_ = self._split_path(path)
        if len(r_) < 2:
            # each file must be associated at least to one tag
            return -errno.ENOENT

        # create inode is it not exist yet
        inode_ = self._get_inode(r_[-1])
        if inode_ is None:
            inode_ = FINode(self._inv_tag_index, r_[-1], mode, dev)
            self._inodes.append(inode_)

        # register tags
        #inode_.add_tag('all')  # fixme: debug, remove ?
        foreach(inode_.add_tag, r_[:-1])

    def unlink(self, path):
        """
        remove an inode, only if it is not associated anymore with
        a tag.
        """
        if DEBUG:
            logging.debug('unlink: path: %s' % path)
        inode_ = self._extract_from_path(path)
        if inode_ is None or (not isinstance(inode_, INode)):
            return
        spath_ =  self._split_path(path)
        for tag_ in spath_[:-1]:
            inode_.remove_tag(tag_)
        if len(inode_._tags) == 0:
            self._inodes.remove(inode_)


    ## extended attributes

    def _is_valid_name(self, name):
        if name == VTagFS.XATTR_NAMESPACE + 'tags':
            return True
        return False

    def getxattr(self, path, name, size):
        if DEBUG:
            logging.debug('getxattr: path: %s :name: %s :size: %d' % (path, name, size))
        if not self._is_valid_name(name):
            return -errno.ENODATA

        inode_ = self._extract_from_path(path)
        if inode_ is None or (not isinstance(inode_, INode)):
            return -errno.ENOENT
        value_ = ' '.join(inode_._tags)
        if size == 0:
            return len(value_)
        return value_

    def listxattr(self, path, size):
        """
        FIXME: this method doesnt work (with gefattr -d smth)
        """
        if DEBUG:
            logging.debug('listxattr: path: %s :size: %d' % (path, size))

        name_ = VTagFS.XATTR_NAMESPACE + 'tags'
        if size == 0:
            return len(name_) + 1
        return [name_]


class File:
    """
    file operations in VTagFS.

    /!\ no data protection i.e. no locking.
    """
    def __init__(self, path, mode, *args):
        """
        There are two uses cases:
        1- for old kernel, the inode is created by mknod, then one instance
           of this class is called for each opening of the file.
        2- on newer kernel, mknod is no longer used, instances of this class
           are used both for create and open operations.

        The distinction between open() and create() is made by checking the
        received mode. Actually we call explicitely mknod() on create(). If
        this is an open() we only get its inode.
        """
        if DEBUG:
            logging.debug('File:init: path: %s :mode: %s :args: %s' % \
                          (path, mode, args))

        if mode & os.O_CREAT:
            # mknod were not called, explicitly call it to create
            # the file (inode).
            ret_ = FUSED.mknod(path, mode, args[0])
            if ret_ is not None:
                # init doesnt return any value, we must raise
                # the error.
                raise OSError, (ret_, '')

        self._inode = inode_ = FUSED._extract_from_path(path)
        # we use a string instead of a real file and StringIO instead
        # of a file object.
        self._fo = StringIO.StringIO(inode_._data)

    def read(self, length, offset):
        if DEBUG:
            logging.debug('File:read: length: %d :offset: %d' % (length, offset))
        self._fo.seek(offset)
        return self._fo.read(length)

    def write(self, buf, offset):
        if DEBUG:
            logging.debug('File:write: buf: %s :offset: %d' % (buf, offset))
        self._fo.seek(offset)
        self._fo.write(buf)
        return len(buf)

    def release(self, flags):
        if DEBUG:
            logging.debug('File:release')
        # dont flush data because flush() is just called before
        self._fo.close()

    def fsync(self, isfsyncfile):
        if DEBUG:
            logging.debug('File:fsync')
        # fixme: correct ?
        self._inode._data = self._fo.getvalue()
        if not isfsyncfile:
            self._inode.update_stat()

    def flush(self):
        if DEBUG:
            logging.debug('File:flush')
        self._inode._data = self._fo.getvalue()

    def fgetattr(self):
        if DEBUG:
            logging.debug('File:fgetattr')
        return self._inode.get_stat()

    def ftruncate(self, len):
        if DEBUG:
            logging.debug('File:ftruncate: len: %d' % len)
        self._fo.truncate(len)


if __name__ == '__main__':
    FUSED = VTagFS()
    FUSED.parse(errex=1)
    FUSED.file_class = File
    FUSED.main()


#     def setxattr(self, path, name, value, size, flags):
#         if DEBUG:
#             logging.debug('setxattr:path: %s' % path)

#         if not self._is_valid_name(name):
#             return -errno.ENODATA

#         inode_ = self._get_inode(path)
#         if inode_ is None:
#             return -errno.ENOENT

#         # whitespace is a value's separator
#         for tag in value.split(' '):
#             if tag != '':
#                 inode_.add_tag(tag)

#     def removexattr(self, path, name):
#         if DEBUG:
#             logging.debug('removexattr:path: %s' % path)

#         if not self._is_valid_name(name):
#             return -errno.ENODATA

#         inode_ = self._get_inode(path)
#         if inode_ is None:
#             return -errno.ENOENT
#         inode_.tags = []
