# -*- coding: utf-8 -*-

# Copyright 2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import os
from calculate.lib.utils.files import readFile
from collections import Mapping, deque
from os import path
import json
from functools import wraps
import fnmatch
import tools


class VFSError(Exception):
    pass


class VFSErrorNotFound(VFSError):
    pass


class VFSErrorExists(VFSError):
    pass


class VFSErrorBadsymlink(VFSError):
    pass


class VFSErrorCycliclink(VFSErrorBadsymlink):
    pass


class VFSErrorNotImplemented(VFSError):
    pass


class VFSErrorRestoreFailed(VFSError):
    pass


class VFS(tools.GenericFs):
    """
    Виртуальная файловая система
    """

    def __init__(self, vfs=None):
        if isinstance(vfs, VFS):
            self.root = vfs.root
        else:
            self.root = VFSRoot()

    def get(self, fn):
        r = self.root
        for dn in (x for x in fn.split('/') if x):
            r = r[dn]
        return r

    def walk(self, sort=False):
        deq = deque((self.root,))
        if sort:
            sortfunc = lambda z: sorted(z, key=lambda y: y.name, reverse=True)
        else:
            sortfunc = iter
        while deq:
            d = deq.pop()
            if isinstance(d, VFSDirectory):
                for x in sortfunc(d):
                    deq.append(x)
            yield d

    def read(self, fn):
        return self.get(fn).read()

    def write(self, fn, data):
        return self.get(fn).write(data)

    def listdir(self, fn, fullpath=False):
        return self.get(fn).listdir(fullpath=fullpath)

    def lexists(self, fn):
        try:
            self.get(fn)
            return True
        except VFSErrorNotFound:
            return False

    def dereflink(self, obj):
        objs = [obj]
        while isinstance(obj, VFSLink):
            obj = obj.deref()
            if obj in objs:
                raise VFSErrorCycliclink(obj.fullname())
        return obj

    def _glob(self, pathname):
        paths = deque(x for x in pathname.split("/") if x)
        pat = paths.popleft()
        SEPARATOR = (None, None)
        deq = deque([("/", self.root), SEPARATOR])
        while deq:
            dirname, d = deq.popleft()
            if d is None:
                if paths:
                    pat = paths.popleft()
                    deq.append(SEPARATOR)
                    continue
                else:
                    break
            if d.isdir():
                dirname = path.join(dirname, d.name)
                for x in d:
                    if fnmatch.fnmatch(x.name, pat):
                        if not paths:
                            yield (path.join(dirname, x.name), x)
                        else:
                            deq.append((dirname, x))

    def glob(self, pathname):
        return (x for x, y in self._glob(pathname))

    def realglob(self, pathname):
        return [y.fullname() for x, y in self._glob(pathname)]

    def exists(self, fn):
        try:
            obj = self.get(fn)
            if isinstance(obj, VFSLink):
                obj = self.dereflink(obj)
            return True
        except (VFSErrorNotFound, VFSErrorBadsymlink):
            return False

    def realpath(self, fn):
        bestpath = fn
        obj = None
        try:
            obj = self.get(fn)
            if isinstance(obj, VFSLink):
                obj = self.dereflink(obj)
            return obj.fullname()
        except VFSErrorBadsymlink:
            if isinstance(obj, VFSLink):
                bestpath = obj.symlink()
        except VFSErrorNotFound:
            pass
        return path.normpath(bestpath)

    # serialization
    def dump(self):
        return self.root.dump()

    def restore(self, dumpdata):
        self.root = self.root.restore(dumpdata)


class SafeVFS(VFS):
    """
    Виртуатльная файловая система, не возвращающая ошибок при обращении к
    отсутстующему файлу
    """
    def exists(self, fn):
        return self.lexists(fn)

    def read(self, fn):
        try:
            return super(SafeVFS, self).read(fn)
        except VFSError:
            return ""

    def listdir(self, fn, fullpath=False):
        try:
            return super(SafeVFS, self).listdir(fn, fullpath)
        except VFSError:
            return []


class VFSObject(object):
    def __init__(self, name):
        self.parent = None
        self.name = name

    def dump(self):
        raise NotImplemented()

    def fullname(self):
        if self.parent:
            dirname = self.parent.fullname()
            dirname = "" if dirname == "/" else dirname
            return "%s/%s" % (dirname, self.name)
        else:
            return self.name

    def read(self):
        raise NotImplemented()

    def write(self, data):
        raise NotImplemented()

    def listdir(self, fullpath=False):
        raise NotImplemented()

    def isdir(self):
        return False

    @classmethod
    def restore(cls, data):
        for clsobj in (VFSFile, VFSLink, VFSDirectory, VFSRoot):
            obj = clsobj.restore(data)
            if obj is not None:
                return obj
        raise VFSErrorRestoreFailed(data)


class VFSDirectory(VFSObject, Mapping):
    def __init__(self, name, *args):
        super(VFSDirectory, self).__init__(name)
        self.children = {}
        for o in args:
            self.append(o)

    def dump(self):
        return ['d', self.name, [x.dump() for x in self]]

    def isdir(self):
        return True

    @classmethod
    def restore(cls, data):
        if data[0] == "d":
            objtype, name, objdata = data
            d = cls(name)
            for x in objdata:
                d.append(VFSObject.restore(x))
            return d

    def __contains__(self, item):
        return item.name in self.children

    def append(self, obj):
        if obj in self:
            raise VFSErrorExists("{dirn}/{name}".format(name=obj.name,
                                                        dirn=self.fullname()))
        self.children[obj.name] = obj
        obj.parent = self

    def __iter__(self):
        return iter(self.children.values())

    def __len__(self):
        return len(self.children)

    def __getitem__(self, item):
        if item == ".":
            return self
        if item == "..":
            return self.parent
        try:
            return self.children[item]
        except KeyError:
            raise VFSErrorNotFound(item)

    def listdir(self, fullpath=False):
        if fullpath:
            dirname = self.fullname()
        else:
            dirname = ""
        return [path.join(dirname, x.name) for x in self.children.values()]

    def read(self):
        raise VFSErrorNotImplemented("Directory does not support reading")

    def write(self, data):
        raise VFSErrorNotImplemented("Directory does not support writing")

    def __eq__(self, obj):
        return self is obj

    def __ne__(self, obj):
        return self is not obj


class VFSRoot(VFSDirectory):
    def __init__(self, *args):
        super(VFSRoot, self).__init__("/", *args)

    def __getitem__(self, item):
        if item == "..":
            return self
        return super(VFSRoot, self).__getitem__(item)

    def dump(self):
        return ['/', self.name, [x.dump() for x in self]]

    @classmethod
    def restore(cls, data):
        if data[0] == "/":
            objtype, name, objdata = data
            d = cls()
            for x in objdata:
                d.append(VFSObject.restore(x))
            return d


class VFSFile(VFSObject):
    def __init__(self, name, data=""):
        super(VFSFile, self).__init__(name)
        self.data = data

    def dump(self):
        return ['r', self.name, self.read()]

    @classmethod
    def restore(cls, data):
        if data[0] == "r":
            objtype, name, objdata = data
            return cls(name, objdata)

    def read(self):
        return self.data

    def write(self, data):
        self.data = data

    def listdir(self, fullpath=False):
        raise VFSErrorNotImplemented("File does not support listing")

    def __getitem__(self, item):
        raise VFSErrorNotImplemented("File does not support listing")


def detect_cyclic(f):
    @wraps(f)
    def wrapper(self, *args, **kw):
        if self.deref_start:
            raise VFSErrorCycliclink(self.fullname())
        self.deref_start = True
        try:
            return f(self, *args, **kw)
        finally:
            self.deref_start = False

    return wrapper


class VFSLink(VFSObject):
    def __init__(self, name, target):
        super(VFSLink, self).__init__(name)
        self.name = name
        self._target = target
        self.deref_start = False

    @classmethod
    def restore(cls, data):
        if data[0] == "l":
            objtype, name, objdata = data
            return cls(name, objdata)

    def dump(self):
        return ['l', self.name, self.symlink()]

    def symlink(self):
        return self._target

    def get_root(self):
        r = self.parent
        while r.parent:
            r = r.parent
        return r

    def deref(self):
        r = self.parent
        if r is None:
            raise VFSErrorBadsymlink(self._target)
        if self._target.startswith("/"):
            r = self.get_root()
        for dn in (x for x in self._target.split('/') if x):
            try:
                r = r[dn]
            except VFSErrorNotFound:
                raise VFSErrorBadsymlink(self._target)
        return r

    @detect_cyclic
    def _isdir(self):
        return self.deref().isdir()

    def isdir(self):
        try:
            return self._isdir()
        except VFSErrorBadsymlink:
            return False

    def __iter__(self):
        if self.isdir():
            return iter(self.deref())
        else:
            raise VFSErrorNotImplemented("Symlink is not directory")

    @detect_cyclic
    def read(self):
        return self.deref().read()

    @detect_cyclic
    def write(self, data):
        return self.deref().write(data)

    @detect_cyclic
    def listdir(self, fullpath=False):
        return self.deref().listdir()

    @detect_cyclic
    def __getitem__(self, item):
        return self.deref().__getitem__(item)


class VFSImporter(object):
    """
    Имортёр файловой системы
    """

    def __init__(self, dn, prefix="/"):
        self.importdn = dn
        self.prefix = prefix

    def importfs(self):
        vfs = VFS()
        dn = self.importdn

        r = vfs.root
        for dns in (x for x in path.relpath(dn, self.prefix).split('/') if x):
            d = VFSDirectory(dns)
            r.append(d)
            r = d

        for root, dnames, fnames in os.walk(dn):
            vroot = path.join("/", path.relpath(root, self.prefix))
            r = vfs.get(vroot)
            remove_dirs = deque()
            remove_files = deque()
            for dn in dnames:
                fdn = path.join(root, dn)
                if path.islink(fdn):
                    self.import_link(r, dn, fdn)
                else:
                    self.import_directory(r, dn, fdn)
                if not self.filter_dirs(root, dn):
                    remove_dirs.append(dn)
            for fn in fnames:
                ffn = path.join(root, fn)
                if path.islink(ffn):
                    self.import_link(r, fn, ffn)
                else:
                    self.import_file(r, fn, ffn)
                if not self.filter_dirs(root, dn):
                    remove_dirs.append(dn)
            while remove_dirs:
                dnames.remove(remove_dirs.popleft())
            while remove_files:
                fnames.remove(remove_files.popleft())
        return vfs

    def filter_dirs(self, root, dn):
        return True

    def filter_files(self, root, fn):
        return True

    def import_link(self, dirobj, fn, ffn):
        target = os.readlink(ffn)
        dirobj.append(VFSLink(fn, target))

    def import_file(self, dirobj, fn, ffn):
        dirobj.append(VFSFile(fn))

    def import_directory(self, dirobj, dn, fdn):
        dirobj.append(VFSDirectory(dn))


class VFSDevfsImporter(VFSImporter):
    """
    Импортер /dev
    """

    def __init__(self, dn="/dev", prefix="/"):
        super(VFSDevfsImporter, self).__init__(dn, prefix)

    def filter_dirs(self, root, dn):
        if dn == "shm":
            return False
        return True


class VFSSysfsImporter(VFSImporter):
    """
    Импортёр /sys
    """

    def __init__(self, dn="/sys", prefix="/"):
        super(VFSSysfsImporter, self).__init__(dn, prefix)

    def import_file(self, dirobj, fn, ffn):
        if fn in ("version", "vendor", "model", "board_name",
                  "board_vendor", "refcnt", "boardname", "size",
                  "removable",
                  "type", "operstate"):
            dirobj.append(VFSFile(fn, readFile(ffn)))
        else:
            dirobj.append(VFSFile(fn))


class VFSJsonKeeper(object):
    """
    Сохранение и загрузыка виртуальной файловой системы
    """

    @staticmethod
    def save(f, vfs):
        json.dump(vfs.dump(), f)

    @staticmethod
    def load(f):
        vfs = VFS()
        vfs.restore(json.load(f))
        return vfs


VFSKeeper = VFSJsonKeeper
