from __future__ import unicode_literals
from __future__ import absolute_import

from .. import namespaces
from ..elements import Attribute
from ..elements.elementbase import LogicElement
from ..tags.context import DataSetter
from ..compat import text_type

from fs.errors import FSError
from fs.path import pathjoin, basename, dirname

import hashlib
import logging

log = logging.getLogger('moya.fs')


class SetContents(LogicElement):
    """Set the contents of a file"""
    xmlns = namespaces.fs

    class Help:
        synopsis = "write data to a file"

    fsobj = Attribute("Filesystem object", required=False, default=None)
    fs = Attribute("Filesystem name", required=False, default=None)
    path = Attribute("Destination path", required=True)
    contents = Attribute("File contents", type="expression", required=True, missing=False)

    def logic(self, context):
        params = self.get_parameters(context)
        params.contents
        if self.has_parameter('fsobj'):
            dst_fs = params.fsobj
        else:
            dst_fs = self.archive.lookup_filesystem(self, params.fs)
        try:
            dst_fs.makedir(dirname(params.path), recursive=True, allow_recreate=True)
            dst_fs.setcontents(params.path, params.contents)
        except Exception as e:
            self.throw("fs.set-contents.fail", "unable to set file contents ({})".format(e))
        log.debug("setcontents '%s'", params.path)


class RemoveFile(LogicElement):
    """Delete a file from a filesystem"""
    xmlns = namespaces.fs

    class Help:
        synopsis = "delete a file"

    fsobj = Attribute("Filesystem object", required=False, default=None)
    fs = Attribute("Filesystem name", required=False, default=None)
    path = Attribute("Destination path", required=True)
    ifexists = Attribute("Only remove if the file exists?", type="boolean", required=False)

    def logic(self, context):
        params = self.get_parameters(context)
        if self.has_parameter('fsobj'):
            dst_fs = params.fsobj
        else:
            dst_fs = self.archive.lookup_filesystem(self, params.fs)
        if params.ifexists and dst_fs.isfile(params.path):
            return
        try:
            dst_fs.remove(params.path)
        except Exception as e:
            self.throw("fs.remove-file.fail",
                       "unable to remove '{}' ({})".format(params.path, e))
        log.debug("removed '%s'", params.path)


class GetSyspath(DataSetter):
    """
    Get a system path for a path in a filesystem.

    A system path (or 'syspath') is a path that maps to the file on the OS filesystem. Not all filesystems can generate syspaths. If Moya is unable to generate a syspath it will throw a [c]get-syspath.no-syspath[/c] exception.

    """
    xmlns = namespaces.fs

    class Help:
        synopsis = "get a system path"
        example = """
        <fs:get-syspath fs="templates" path="index.html" dst="index_template_path"/>
        """

    fsobj = Attribute("Filesystem object", required=False, default=None)
    fs = Attribute("Filesystem name", required=False, default=None)
    path = Attribute("Destination path", required=True)

    def logic(self, context):
        params = self.get_parameters(context)

        if self.has_parameter('fsobj'):
            dst_fs = params.fsobj
        else:
            dst_fs = self.archive.lookup_filesystem(self, params.fs)

        try:
            syspath = dst_fs.getsyspath(params.path)
        except:
            self.throw('fs.get-syspath.no-syspath',
                       "{!r} can not generate a syspath for '{}'".format(dst_fs, params.path))
        self.set_context(context, self.dst(context), syspath)


class GetMD5(DataSetter):
    """Get the MD5 of a file"""
    xmlns = namespaces.fs

    class Help:
        synopsis = "get the md5 of a file"

    fsobj = Attribute("Filesystem object", required=False, default=None)
    fs = Attribute("Filesystem name", required=False, default=None)
    path = Attribute("Path of file", required=True)

    def get_value(self, context):
        params = self.get_parameters(context)
        if self.has_parameter('fsobj'):
            fs = params.fsobj
        else:
            fs = self.archive.lookup_filesystem(self, params.fs)

        m = hashlib.md5()
        try:
            with fs.open(params.path, 'rb') as f:
                while 1:
                    chunk = f.read(16384)
                    if not chunk:
                        break
                    m.update(chunk)
        except FSError:
            self.throw("fs.get-md5.fail", "unable to read file '{}'".format(params.path))
        else:
            return m.hexdigest()


class GetInfo(DataSetter):
    xmlns = namespaces.fs

    class Help:
        synopsis = "get an info object for a file"

    fsobj = Attribute("Filesystem object", required=False, default=None)
    fs = Attribute("Filesystem name", required=False, default=None)
    path = Attribute("Path of file", type="expression", required=True)

    def get_value(self, context):
        params = self.get_parameters(context)
        if self.has_parameter('fsobj'):
            fs = params.fsobj
        else:
            fs = self.archive.lookup_filesystem(self, params.fs)

        try:
            info = fs.getinfo(params.path)
        except FSError:
            self.throw('fs.get-info.fail', "unable to get info for path '{}'".format(params.path))
        else:
            return info


class GetSize(DataSetter):
    xmlns = namespaces.fs

    class Help:
        synopsis = "get the size of a file"

    fsobj = Attribute("Filesystem object", required=False, default=None)
    fs = Attribute("Filesystem name", required=False, default=None)
    path = Attribute("Path of file", type="expression", required=True)

    def get_value(self, context):
        params = self.get_parameters(context)
        path = text_type(params.path)
        if self.has_parameter('fsobj'):
            fs = params.fsobj
        else:
            fs = self.archive.lookup_filesystem(self, params.fs)

        try:
            info = fs.getsize(path)
        except FSError:
            self.throw('fs.get-size.fail', "unable to get info for path '{}'".format(params.path))
        else:
            return info


class Walk(DataSetter):
    """Get a list of files"""
    xmlns = namespaces.fs

    class Help:
        synopsis = "go through the files in a directory"

    fsobj = Attribute("Filesystem object", required=False, default=None)
    path = Attribute("Path to walk", required=False, default="/")
    fs = Attribute("Filesystem name", required=False, default=None)
    dirs = Attribute("Filter directories (function should reference 'name' and return a boolean)", type="function", default=None)
    files = Attribute("Filter files (function should reference 'name' and return a boolean)", type="function", default=None)
    search = Attribute("Search method", default="breadth", choices=["breadth", "depth"])
    dst = Attribute("Destination", required=True, type="reference")

    def logic(self, context):
        params = self.get_parameters(context)
        if self.has_parameter('fsobj'):
            walk_fs = params.fsobj
        else:
            walk_fs = self.archive.get_filesystem(params.fs)

        wildcard = lambda name: params.files(context, name=basename(name)) if self.has_parameter('files') else lambda name: True
        dir_wildcard = lambda name: params.dirs(context, name=basename(name)) if self.has_parameter('dirs') else lambda name: True

        paths = []
        add_path = paths.append

        for dirname, dir_paths in walk_fs.walk(path=params.path,
                                               search=params.search,
                                               wildcard=wildcard,
                                               dir_wildcard=dir_wildcard,
                                               ignore_errors=True):

            for path in dir_paths:
                add_path(pathjoin(dirname, path))
        self.set_context(context, params.dst, paths)
