from fnmatch import fnmatch
import sys

from pyrsistent import pset
import attr

from filesystems import exceptions


_PY3 = sys.version_info[0] >= 3


def _recursive_remove(fs, path):
    """
    A recursive, non-atomic directory removal.

    """

    if not fs.is_link(path=path) and fs.is_dir(path=path):
        for child in fs.children(path=path):
            _recursive_remove(fs=fs, path=child)
        fs.remove_empty_directory(path=path)
    else:
        fs.remove_file(path=path)


def create(
    name,

    open_file,
    create_file,
    remove_file,

    create_directory,
    list_directory,
    remove_empty_directory,
    temporary_directory,

    link,
    readlink,
    realpath,

    exists,
    is_dir,
    is_file,
    is_link,

    remove=_recursive_remove,

    state=lambda: None,
):
    """
    Create a new kind of filesystem.

    """

    return attr.s(hash=True)(
        type(
            name, (object,), dict(
                _state=attr.ib(default=attr.Factory(state), repr=False),

                create=create_file,
                open=lambda fs, path, mode="r": open_file(
                    fs=fs, path=path, mode=mode,
                ),
                remove_file=remove_file,

                remove=remove,

                create_directory=create_directory,
                list_directory=list_directory,
                remove_empty_directory=remove_empty_directory,
                temporary_directory=temporary_directory,

                link=link,
                readlink=readlink,
                realpath=realpath,

                exists=exists,
                is_dir=is_dir,
                is_file=is_file,
                is_link=is_link,

                touch=_touch,
                children=_children,
                glob_children=_glob_children,
                contents_of=_open_and_read,
            ),
        ),
    )


def _children(fs, path):
    return pset(path.descendant(p) for p in fs.list_directory(path=path))


def _glob_children(fs, path, glob):
    return pset(
        path.descendant(p)
        for p in fs.list_directory(path=path)
        if fnmatch(p, glob)
    )


def _touch(fs, path):
    fs.open(path=path, mode="wb").close()


def _open_and_read(fs, path):
    with fs.open(path=path) as file:
        return file.read()


@attr.s(frozen=True)
class FileMode(object):
    activity = attr.ib(default='r')
    mode = attr.ib(
        default='',
        convert=lambda x: x if x != '' else ('t' if _PY3 else 'b'),
    )
    read = attr.ib()
    write = attr.ib()
    append = attr.ib()
    text = attr.ib()
    binary = attr.ib()

    @read.default
    def _(self):
        return self.activity == 'r'

    @write.default
    def _(self):
        return self.activity == 'w'

    @append.default
    def _(self):
        return self.activity == 'a'

    @text.default
    def _(self):
        return self.mode == 't'

    @binary.default
    def _(self):
        return self.mode == 'b'

    @activity.validator
    def _(self, attribute, value):
        options = 'rwa'

        if value not in options:
            raise exceptions.InvalidMode(
                'Mode must start with one of {} but found {}'.format(
                    repr(options),
                    repr(value),
                )
            )

    @mode.validator
    def _(self, attribute, value):
        options = 'bt'

        if value not in options:
            raise exceptions.InvalidMode(
                'Mode must start with one of {} but found {}'.format(
                    repr(options),
                    repr(value),
                )
            )

    def io_open_string(self):
        return self.activity + self.mode


def normalize_mode(mode):
    parameters = {}
    first = mode[:1]
    rest = mode[1:]

    if len(first) > 0:
        parameters['activity'] = first

        if len(rest) > 0:
            parameters['mode'] = rest

    return FileMode(**parameters)

@@ -2386,19 +2386,19 @@
 default=
-'r'
+%22r%22
 )%0A    mo
@@ -2471,20 +2471,20 @@
  != 
-''
+%22%22
  else (
-'t'
+%22t%22
  if 
@@ -2493,19 +2493,19 @@
 Y3 else 
-'b'
+%22b%22
 ),%0A    )
@@ -2687,11 +2687,11 @@
  == 
-'r'
+%22r%22
 %0A%0A  
@@ -2760,11 +2760,11 @@
  == 
-'w'
+%22w%22
 %0A%0A  
@@ -2834,11 +2834,11 @@
  == 
-'a'
+%22a%22
 %0A%0A  
@@ -2902,11 +2902,11 @@
  == 
-'t'
+%22t%22
 %0A%0A  
@@ -2972,11 +2972,11 @@
  == 
-'b'
+%22b%22
 %0A%0A  
@@ -3054,13 +3054,13 @@
 s = 
-'rwa'
+%22rwa%22
 %0A%0A  
@@ -3140,33 +3140,33 @@
                 
-'
+%22
 Mode must start 
@@ -3184,33 +3184,33 @@
  %7B%7D but found %7B%7D
-'
+%22
 .format(%0A       
@@ -3380,12 +3380,12 @@
 s = 
-'bt'
+%22bt%22
 %0A%0A  
@@ -3473,17 +3473,17 @@
         
-'
+%22
 Mode mus
@@ -3517,17 +3517,17 @@
 found %7B%7D
-'
+%22
 .format(
@@ -3835,17 +3835,17 @@
 ers%5B
-'
+%22
 activity
 '%5D =
@@ -3840,17 +3840,17 @@
 activity
-'
+%22
 %5D = firs
@@ -3905,14 +3905,14 @@
 ers%5B
-'
+%22
 mode
-'
+%22
 %5D = 

Switch to double quotes