#!/usr/bin/env python
#
# pip install fusepy
#
# This implementation could improve a /lot/, but the core library is missing
# some functionality (#213) to make that easy. Additionally it needs a set of
# Python bindings for FUSE that stupidly require use of a thread pool.

from __future__ import absolute_import, division
from __future__ import unicode_literals

import errno
import logging
import threading
import sys
import time

import fuse
import mitogen.master
import mitogen.utils

import __main__
import os


LOG = logging.getLogger(__name__)


def to_text(p):
    """
    On 3.x, fusepy returns paths as bytes.
    """
    if isinstance(p, bytes):
        return p.decode('utf-8')
    return p


def errno_wrap(modname, func, *args):
    try:
        return getattr(globals()[modname], func)(*args), None
    except (IOError, OSError):
        e = sys.exc_info()[1]
        if e.args[0] == errno.ENOENT:
            LOG.error('%r(**%r): %s', func, args, e)
        else:
            LOG.exception('While running %r(**%r)', func, args)
        return None, to_text(errno.errorcode[e.args[0]])


def errno_call(context, func, *args):
    result, errname = context.call(
        errno_wrap,
        func.__module__,
        func.__name__,
        *args
    )
    if errname:
        raise fuse.FuseOSError(getattr(errno, errname))
    return result


def _create(path, mode):
    fd = os.open(path, os.O_WRONLY)
    try:
        os.fchmod(fd, mode)
    finally:
        os.close(fd)


def _stat(path):
    st = os.lstat(path)
    keys = ('st_atime', 'st_gid', 'st_mode', 'st_mtime', 'st_size', 'st_uid')
    dct = dict((key, getattr(st, key)) for key in keys)
    dct['has_contents'] = os.path.exists(os.path.join(path, 'Contents'))
    return dct


def _listdir(path):
    return [
        (name, _stat(os.path.join(path, name)), 0)
        for name in os.listdir(path)
    ]


def _read(path, size, offset):
    fd = os.open(path, os.O_RDONLY)
    try:
        os.lseek(fd, offset, os.SEEK_SET)
        return os.read(fd, size)
    finally:
        os.close(fd)


def _truncate(path, length):
    fd = os.open(path, os.O_RDWR)
    try:
        os.truncate(fd, length)
    finally:
        os.close(fd)


def _write(path, data, offset):
    fd = os.open(path, os.O_RDWR)
    try:
        os.lseek(fd, offset, os.SEEK_SET)
        return os.write(fd, data)
    finally:
        os.close(fd)


def _evil_name(path):
    if not (os.path.basename(path).startswith('._') or
            path.endswith('.DS_Store')):
        return
    raise fuse.FuseOSError(errno.ENOENT)


def _chroot(path):
    os.chroot(path)


class Operations(fuse.Operations):  # fuse.LoggingMixIn,
    def __init__(self, host, path='.'):
        self.host = host
        self.root = path
        self.ready = threading.Event()
        if not hasattr(self, 'encoding'):
            self.encoding = 'utf-8'

    def init(self, path):
        self.broker = mitogen.master.Broker(install_watcher=False)
        self.router = mitogen.master.Router(self.broker)
        self.host = self.router.ssh(hostname=self.host)
        self._context = self.router.sudo(via=self.host)
        #self._context.call(_chroot , '/home/dmw')
        self._stat_cache = {}
        self.ready.set()

    def destroy(self, path):
        self.broker.shutdown()

    @property
    def context(self):
        self.ready.wait()
        return self._context

    def chmod(self, path, mode):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, os.chmod, path, mode)

    def chown(self, path, uid, gid):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, os.chown, path, uid, gid)

    def create(self, path, mode):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, _create, path, mode) or 0

    def getattr(self, path, fh=None):
        _evil_name(path)
        if path in self._stat_cache:
            now = time.time()
            then, st = self._stat_cache[path]
            if now < (then + 2.0):
                return st
        basedir = os.path.dirname(path)
        if path.endswith('/Contents') and basedir in self._stat_cache:
            now = time.time()
            then, st = self._stat_cache[basedir]
            if now < (then + 2.0) and not st['has_contents']:
                raise fuse.FuseOSError(errno.ENOENT)

        return errno_call(self._context, _stat, path)

    def mkdir(self, path, mode):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, os.mkdir, path, mode)

    def read(self, path, size, offset, fh):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, _read, path, size, offset)

    def readdir(self, path, fh):
        _evil_name(path)
        lst = errno_call(self._context, _listdir, path)
        now = time.time()
        for name, stat, _ in lst:
            self._stat_cache[os.path.join(path, name)] = (now, stat)
        return lst

    def readlink(self, path):
        _evil_name(path)
        return errno_call(self._context, os.readlink, path)

    def rename(self, old, new):
        old = old.decode(self.encoding)
        new = new.decode(self.encoding)
        return errno_call(self._context, os.rename, old, new)
        # TODO return self.sftp.rename(old, self.root + new)

    def rmdir(self, path):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, os.rmdir, path)

    def symlink(self, target, source):
        target = target.decode(self.encoding)
        source = source.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, os.symlink, source, target)

    def truncate(self, path, length, fh=None):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, _truncate, path, length)

    def unlink(self, path):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, os.unlink, path)

    def utimens(self, path, times=None):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, os.utime, path, times)

    def write(self, path, data, offset, fh):
        path = path.decode(self.encoding)
        _evil_name(path)
        return errno_call(self._context, _write, path, data, offset)


@mitogen.main(log_level='DEBUG')
def main(router):
    if len(sys.argv) != 3:
        print('usage: %s <host> <mountpoint>' % sys.argv[0])
        sys.exit(1)

    kwargs = {}
    if sys.platform == 'darwin':
        kwargs['volname'] = '%s (Mitogen)' % (sys.argv[1],)

    fuse.FUSE(
        operations=Operations(sys.argv[1]),
        mountpoint=sys.argv[2],
        foreground=True,
        **kwargs
    )
