"""
Defines the Exception classes thrown by PyFilesystem objects. Exceptions relating
to the underlying filesystem are translated in to one of the following Exceptions.
Exceptions that relate to a path store that path in `self.path`.

All Exception classes are derived from `FSError` which can be used as a
catch-all exception.

"""

__all__ = ['FSError',
           'CreateFailedError',
           'PathError',
           'InvalidPathError',
           'InvalidCharsInPathError',
           'OperationFailedError',
           'UnsupportedError',
           'RemoteConnectionError',
           'StorageSpaceError',
           'PermissionDeniedError',
           'FSClosedError',
           'OperationTimeoutError',
           'RemoveRootError',
           'ResourceError',
           'NoSysPathError',
           'NoMetaError',
           'NoPathURLError',
           'ResourceNotFoundError',
           'ResourceInvalidError',
           'DestinationExistsError',
           'DirectoryNotEmptyError',
           'ParentDirectoryMissingError',
           'ResourceLockedError',
           'NoMMapError',
           'BackReferenceError',
           'convert_fs_errors',
           'convert_os_errors',
           ]

import sys
import errno
import six

from fs.path import *
from fs.local_functools import wraps


class FSError(Exception):
    """Base exception class for the FS module."""
    default_message = "Unspecified error"

    def __init__(self,msg=None,details=None):
        if msg is None:
            msg = self.default_message
        self.msg = msg
        self.details = details

    def __str__(self):
        keys = {}
        for k,v in self.__dict__.items():
            if isinstance(v,str):
                v = v.encode(sys.getfilesystemencoding())
            keys[k] = v
        return str(self.msg % keys)

    def __unicode__(self):
        keys = {}
        for k,v in self.__dict__.items():
            if isinstance(v, six.binary_type):
                v = v.decode(sys.getfilesystemencoding(), 'replace')
            keys[k] = v
        return str(self.msg, encoding=sys.getfilesystemencoding(), errors='replace') % keys

    def __reduce__(self):
        return (self.__class__,(),self.__dict__.copy(),)


class CreateFailedError(FSError):
    """An exception thrown when a FS could not be created"""
    default_message = "Unable to create filesystem"


class PathError(FSError):
    """Exception for errors to do with a path string.
    """
    default_message = "Path is invalid: %(path)s"

    def __init__(self,path="",**kwds):
        self.path = path
        super(PathError,self).__init__(**kwds)


class InvalidPathError(PathError):
    """Base exception for fs paths that can't be mapped on to the underlaying filesystem."""
    default_message = "Path is invalid on this filesystem %(path)s"


class InvalidCharsInPathError(InvalidPathError):
    """The path contains characters that are invalid on this filesystem"""
    default_message = "Path contains invalid characters: %(path)s"


class OperationFailedError(FSError):
    """Base exception class for errors associated with a specific operation."""
    default_message = "Unable to %(opname)s: unspecified error [%(errno)s - %(details)s]"

    def __init__(self,opname="",path=None,**kwds):
        self.opname = opname
        self.path = path
        self.errno = getattr(kwds.get("details",None),"errno",None)
        super(OperationFailedError,self).__init__(**kwds)


class UnsupportedError(OperationFailedError):
    """Exception raised for operations that are not supported by the FS."""
    default_message = "Unable to %(opname)s: not supported by this filesystem"


class RemoteConnectionError(OperationFailedError):
    """Exception raised when operations encounter remote connection trouble."""
    default_message = "%(opname)s: remote connection errror"


class StorageSpaceError(OperationFailedError):
    """Exception raised when operations encounter storage space trouble."""
    default_message = "Unable to %(opname)s: insufficient storage space"


class PermissionDeniedError(OperationFailedError):
    default_message = "Unable to %(opname)s: permission denied"


class FSClosedError(OperationFailedError):
    default_message = "Unable to %(opname)s: the FS has been closed"


class OperationTimeoutError(OperationFailedError):
    default_message = "Unable to %(opname)s: operation timed out"


class RemoveRootError(OperationFailedError):
    default_message = "Can't remove root dir"


class ResourceError(FSError):
    """Base exception class for error associated with a specific resource."""
    default_message = "Unspecified resource error: %(path)s"

    def __init__(self,path="",**kwds):
        self.path = path
        self.opname = kwds.pop("opname",None)
        super(ResourceError,self).__init__(**kwds)


class NoSysPathError(ResourceError):
    """Exception raised when there is no syspath for a given path."""
    default_message = "No mapping to OS filesystem: %(path)s"


class NoMetaError(FSError):
    """Exception raised when there is no meta value available."""
    default_message = "No meta value named '%(meta_name)s' could be retrieved"
    def __init__(self, meta_name, msg=None):
        self.meta_name = meta_name
        super(NoMetaError, self).__init__(msg)
    def __reduce__(self):
        return (self.__class__,(self.meta_name,),self.__dict__.copy(),)


class NoPathURLError(ResourceError):
    """Exception raised when there is no URL form for a given path."""
    default_message = "No URL form: %(path)s"


class ResourceNotFoundError(ResourceError):
    """Exception raised when a required resource is not found."""
    default_message = "Resource not found: %(path)s"


class ResourceInvalidError(ResourceError):
    """Exception raised when a resource is the wrong type."""
    default_message = "Resource is invalid: %(path)s"


class DestinationExistsError(ResourceError):
    """Exception raised when a target destination already exists."""
    default_message = "Destination exists: %(path)s"


class DirectoryNotEmptyError(ResourceError):
    """Exception raised when a directory to be removed is not empty."""
    default_message = "Directory is not empty: %(path)s"


class ParentDirectoryMissingError(ResourceError):
    """Exception raised when a parent directory is missing."""
    default_message = "Parent directory is missing: %(path)s"


class ResourceLockedError(ResourceError):
    """Exception raised when a resource can't be used because it is locked."""
    default_message = "Resource is locked: %(path)s"


class NoMMapError(ResourceError):
    """Exception raise when getmmap fails to create a mmap"""
    default_message = "Can't get mmap for %(path)s"


class BackReferenceError(ValueError):
    """Exception raised when too many backrefs exist in a path (ex: '/..', '/docs/../..')."""


def convert_fs_errors(func):
    """Function wrapper to convert FSError instances into OSError."""
    @wraps(func)
    def wrapper(*args,**kwds):
        try:
            return func(*args,**kwds)
        except ResourceNotFoundError as e:
            raise OSError(errno.ENOENT,str(e))
        except ParentDirectoryMissingError as e:
            if sys.platform == "win32":
                raise OSError(errno.ESRCH,str(e))
            else:
                raise OSError(errno.ENOENT,str(e))
        except ResourceInvalidError as e:
            raise OSError(errno.EINVAL,str(e))
        except PermissionDeniedError as e:
            raise OSError(errno.EACCES,str(e))
        except ResourceLockedError as e:
            if sys.platform == "win32":
                raise WindowsError(32,str(e))
            else:
                raise OSError(errno.EACCES,str(e))
        except DirectoryNotEmptyError as e:
            raise OSError(errno.ENOTEMPTY,str(e))
        except DestinationExistsError as e:
            raise OSError(errno.EEXIST,str(e))
        except StorageSpaceError as e:
            raise OSError(errno.ENOSPC,str(e))
        except RemoteConnectionError as e:
            raise OSError(errno.ENETDOWN,str(e))
        except UnsupportedError as e:
            raise OSError(errno.ENOSYS,str(e))
        except FSError as e:
            raise OSError(errno.EFAULT,str(e))
    return wrapper


def convert_os_errors(func):
    """Function wrapper to convert OSError/IOError instances into FSError."""
    opname = func.__name__
    @wraps(func)
    def wrapper(self,*args,**kwds):
        try:
            return func(self,*args,**kwds)
        except (OSError,IOError) as e:
            (exc_type,exc_inst,tb) = sys.exc_info()
            path = getattr(e,"filename",None)
            if path and path[0] == "/" and hasattr(self,"root_path"):
                path = normpath(path)
                if isprefix(self.root_path,path):
                    path = path[len(self.root_path):]
            if not hasattr(e,"errno") or not e.errno:
                raise OperationFailedError(opname,details=e).with_traceback(tb)
            if e.errno == errno.ENOENT:
                raise ResourceNotFoundError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.EFAULT:
                # This can happen when listdir a directory that is deleted by another thread
                # Best to interpret it as a resource not found
                raise ResourceNotFoundError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.ESRCH:
                raise ResourceNotFoundError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.ENOTEMPTY:
                raise DirectoryNotEmptyError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.EEXIST:
                raise DestinationExistsError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == 183: # some sort of win32 equivalent to EEXIST
                raise DestinationExistsError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.ENOTDIR:
                raise ResourceInvalidError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.EISDIR:
                raise ResourceInvalidError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.EINVAL:
                raise ResourceInvalidError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.ENOSPC:
                raise StorageSpaceError(opname,path=path,details=e).with_traceback(tb)
            if e.errno == errno.EPERM:
                raise PermissionDeniedError(opname,path=path,details=e).with_traceback(tb)
            if hasattr(errno,"ENONET") and e.errno == errno.ENONET:
                raise RemoteConnectionError(opname,path=path,details=e).with_traceback(tb)
            if e.errno == errno.ENETDOWN:
                raise RemoteConnectionError(opname,path=path,details=e).with_traceback(tb)
            if e.errno == errno.ECONNRESET:
                raise RemoteConnectionError(opname,path=path,details=e).with_traceback(tb)
            if e.errno == errno.EACCES:
                if sys.platform == "win32":
                    if e.args[0] and e.args[0] == 32:
                        raise ResourceLockedError(path,opname=opname,details=e).with_traceback(tb)
                raise PermissionDeniedError(opname,details=e).with_traceback(tb)
            # Sometimes windows gives some random errors...
            if sys.platform == "win32":
                if e.errno in (13,):
                    raise ResourceInvalidError(path,opname=opname,details=e).with_traceback(tb)
            if e.errno == errno.ENAMETOOLONG:
                raise PathError(path,details=e).with_traceback(tb)
            if e.errno == errno.EOPNOTSUPP:
                raise UnsupportedError(opname,details=e).with_traceback(tb)
            if e.errno == errno.ENOSYS:
                raise UnsupportedError(opname,details=e).with_traceback(tb)
            raise OperationFailedError(opname,details=e).with_traceback(tb)
    return wrapper


