"""Exception classes thrown by filesystem operations.

Errors relating to the underlying filesystem are translated in
to one of the following exceptions.

All Exception classes are derived from `~fs.errors.FSError`
which may be used as a catch-all filesystem exception.

"""

from __future__ import print_function, unicode_literals

import typing

import functools
import six
from six import text_type

if typing.TYPE_CHECKING:
    from typing import Optional, Text


__all__ = [
    "BulkCopyFailed",
    "CreateFailed",
    "DestinationExists",
    "DirectoryExists",
    "DirectoryExpected",
    "DirectoryNotEmpty",
    "FileExists",
    "FileExpected",
    "FilesystemClosed",
    "FSError",
    "IllegalBackReference",
    "InsufficientStorage",
    "InvalidCharsInPath",
    "InvalidPath",
    "MissingInfoNamespace",
    "NoSysPath",
    "NoURL",
    "OperationFailed",
    "OperationTimeout",
    "PathError",
    "PermissionDenied",
    "RemoteConnectionError",
    "RemoveRootError",
    "ResourceError",
    "ResourceInvalid",
    "ResourceLocked",
    "ResourceNotFound",
    "ResourceReadOnly",
    "Unsupported",
    "UnsupportedHash",
]


class MissingInfoNamespace(AttributeError):
    """An expected namespace is missing."""

    def __init__(self, namespace):  # noqa: D107
        # type: (Text) -> None
        self.namespace = namespace
        msg = "namespace '{}' is required for this attribute"
        super(MissingInfoNamespace, self).__init__(msg.format(namespace))

    def __reduce__(self):
        return type(self), (self.namespace,)


@six.python_2_unicode_compatible
class FSError(Exception):
    """Base exception for the `fs` module."""

    default_message = "Unspecified error"

    def __init__(self, msg=None):  # noqa: D107
        # type: (Optional[Text]) -> None
        self._msg = msg or self.default_message
        super(FSError, self).__init__()

    def __str__(self):
        # type: () -> Text
        """Return the error message."""
        msg = self._msg.format(**self.__dict__)
        return msg

    def __repr__(self):
        # type: () -> Text
        msg = self._msg.format(**self.__dict__)
        return "{}({!r})".format(self.__class__.__name__, msg)


class FilesystemClosed(FSError):
    """Attempt to use a closed filesystem."""

    default_message = "attempt to use closed filesystem"


class BulkCopyFailed(FSError):
    """A copy operation failed in worker threads."""

    default_message = "One or more copy operations failed (see errors attribute)"

    def __init__(self, errors):  # noqa: D107
        self.errors = errors
        super(BulkCopyFailed, self).__init__()


class CreateFailed(FSError):
    """Filesystem could not be created."""

    default_message = "unable to create filesystem, {details}"

    def __init__(self, msg=None, exc=None):  # noqa: D107
        # type: (Optional[Text], Optional[Exception]) -> None
        self._msg = msg or self.default_message
        self.details = "" if exc is None else text_type(exc)
        self.exc = exc

    @classmethod
    def catch_all(cls, func):
        @functools.wraps(func)
        def new_func(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except cls:
                raise
            except Exception as e:
                raise cls(exc=e)

        return new_func  # type: ignore

    def __reduce__(self):
        return type(self), (self._msg, self.exc)


class PathError(FSError):
    """Base exception for errors to do with a path string."""

    default_message = "path '{path}' is invalid"

    def __init__(self, path, msg=None, exc=None):  # noqa: D107
        # type: (Text, Optional[Text], Optional[Exception]) -> None
        self.path = path
        self.exc = exc
        super(PathError, self).__init__(msg=msg)

    def __reduce__(self):
        return type(self), (self.path, self._msg, self.exc)


class NoSysPath(PathError):
    """The filesystem does not provide *sys paths* to the resource."""

    default_message = "path '{path}' does not map to the local filesystem"


class NoURL(PathError):
    """The filesystem does not provide an URL for the resource."""

    default_message = "path '{path}' has no '{purpose}' URL"

    def __init__(self, path, purpose, msg=None):  # noqa: D107
        # type: (Text, Text, Optional[Text]) -> None
        self.purpose = purpose
        super(NoURL, self).__init__(path, msg=msg)

    def __reduce__(self):
        return type(self), (self.path, self.purpose, self._msg)


class InvalidPath(PathError):
    """Path can't be mapped on to the underlaying filesystem."""

    default_message = "path '{path}' is invalid on this filesystem "


class InvalidCharsInPath(InvalidPath):
    """Path contains characters that are invalid on this filesystem."""

    default_message = "path '{path}' contains invalid characters"


class OperationFailed(FSError):
    """A specific operation failed."""

    default_message = "operation failed, {details}"

    def __init__(
        self,
        path=None,  # type: Optional[Text]
        exc=None,  # type: Optional[Exception]
        msg=None,  # type: Optional[Text]
    ):  # noqa: D107
        # type: (...) -> None
        self.path = path
        self.exc = exc
        self.details = "" if exc is None else text_type(exc)
        self.errno = getattr(exc, "errno", None)
        super(OperationFailed, self).__init__(msg=msg)

    def __reduce__(self):
        return type(self), (self.path, self.exc, self._msg)


class Unsupported(OperationFailed):
    """Operation not supported by the filesystem."""

    default_message = "not supported"


class RemoteConnectionError(OperationFailed):
    """Operations encountered remote connection trouble."""

    default_message = "remote connection error"


class InsufficientStorage(OperationFailed):
    """Storage is insufficient for requested operation."""

    default_message = "insufficient storage space"


class PermissionDenied(OperationFailed):
    """Not enough permissions."""

    default_message = "permission denied"


class OperationTimeout(OperationFailed):
    """Filesystem took too long."""

    default_message = "operation timed out"


class RemoveRootError(OperationFailed):
    """Attempt to remove the root directory."""

    default_message = "root directory may not be removed"


class ResourceError(FSError):
    """Base exception class for error associated with a specific resource."""

    default_message = "failed on path {path}"

    def __init__(self, path, exc=None, msg=None):  # noqa: D107
        # type: (Text, Optional[Exception], Optional[Text]) -> None
        self.path = path
        self.exc = exc
        super(ResourceError, self).__init__(msg=msg)

    def __reduce__(self):
        return type(self), (self.path, self.exc, self._msg)


class ResourceNotFound(ResourceError):
    """Required resource not found."""

    default_message = "resource '{path}' not found"


class ResourceInvalid(ResourceError):
    """Resource has the wrong type."""

    default_message = "resource '{path}' is invalid for this operation"


class FileExists(ResourceError):
    """File already exists."""

    default_message = "resource '{path}' exists"


class FileExpected(ResourceInvalid):
    """Operation only works on files."""

    default_message = "path '{path}' should be a file"


class DirectoryExpected(ResourceInvalid):
    """Operation only works on directories."""

    default_message = "path '{path}' should be a directory"


class DestinationExists(ResourceError):
    """Target destination already exists."""

    default_message = "destination '{path}' exists"


class DirectoryExists(ResourceError):
    """Directory already exists."""

    default_message = "directory '{path}' exists"


class DirectoryNotEmpty(ResourceError):
    """Attempt to remove a non-empty directory."""

    default_message = "directory '{path}' is not empty"


class ResourceLocked(ResourceError):
    """Attempt to use a locked resource."""

    default_message = "resource '{path}' is locked"


class ResourceReadOnly(ResourceError):
    """Attempting to modify a read-only resource."""

    default_message = "resource '{path}' is read only"


class IllegalBackReference(ValueError):
    """Too many backrefs exist in a path.

    This error will occur if the back references in a path would be
    outside of the root. For example, ``"/foo/../../"``, contains two back
    references which would reference a directory above the root.

    Note:
        This exception is a subclass of `ValueError` as it is not
        strictly speaking an issue with a filesystem or resource.

    """

    def __init__(self, path):  # noqa: D107
        # type: (Text) -> None
        self.path = path
        msg = ("path '{path}' contains back-references outside of filesystem").format(
            path=path
        )
        super(IllegalBackReference, self).__init__(msg)

    def __reduce__(self):
        return type(self), (self.path,)


class UnsupportedHash(ValueError):
    """The requested hash algorithm is not supported.

    This exception will be thrown if a hash algorithm is requested that is
    not supported by hashlib.

    """
