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 dirname
import fs.walk
from fs import wildcard

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.makedirs(dirname(params.path), recreate=True)
            if hasattr(params.contents, "read"):
                dst_fs.setfile(params.path, params.contents)
            elif isinstance(params.contents, bytes):
                dst_fs.setfile(params.path, params.contents)
            elif isinstance(params.contents, text_type):
                dst_fs.settext(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 MoyaWalker(fs.walk.Walker):
    def __init__(self, exclude_dirs):
        self.exclude_dirs = exclude_dirs

    def check_open_dir(self, fs, info):
        return not self.exclude_dirs(info.name)


class WalkFiles(DataSetter):
    """Recursively get a list of files in a filesystem."""

    xmlns = namespaces.fs

    class Help:
        synopsis = "recursively list 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)
    files = Attribute(
        'One or more wildcards to filter results by, e.g "*.py, *.js"',
        type="commalist",
        default="*",
    )
    excludedirs = Attribute(
        'Directory wildcards to exclude form walk, e.g. "*.git, *.svn"',
        type="commalist",
        default=None,
    )
    search = Attribute(
        "Search method ('breadth' or 'depth')",
        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)
        if params.excludedirs:
            walker = MoyaWalker(wildcard.get_matcher(params.excludedirs, True))
        else:
            walker = fs.walk.Walker()
        paths = list(
            walker.files(
                walk_fs, params.path, search=params.search, filter=params.files or None
            )
        )
        self.set_context(context, params.dst, paths)
