"""
A helper module that can work with paths
that can refer to data inside a zipfile

XXX: Need to determine if isdir("zipfile.zip")
should return True or False. Currently returns
True, but that might do the wrong thing with
data-files that are zipfiles.
"""
import os as _os
import zipfile as _zipfile
import errno as _errno
import time as _time
import sys as _sys
import stat as _stat

_DFLT_DIR_MODE = (
    _stat.S_IXOTH
    | _stat.S_IXGRP
    | _stat.S_IXUSR
    | _stat.S_IROTH
    | _stat.S_IRGRP
    | _stat.S_IRUSR)

_DFLT_FILE_MODE = (
    _stat.S_IROTH
    | _stat.S_IRGRP
    | _stat.S_IRUSR)


if _sys.version_info[0] == 2:
    from StringIO import StringIO as _BaseStringIO
    from StringIO import StringIO as _BaseBytesIO

    class _StringIO (_BaseStringIO):
        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_value, traceback):
            self.close()
            return False

    class _BytesIO (_BaseBytesIO):
        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_value, traceback):
            self.close()
            return False

else:
    from io import StringIO as _StringIO
    from io import BytesIO as _BytesIO


def _locate(path):
    full_path = path
    if _os.path.exists(path):
        return path, None

    else:
        rest = []
        root = _os.path.splitdrive(path)
        while path and path != root:
            path, bn = _os.path.split(path)
            rest.append(bn)
            if _os.path.exists(path):
                break

        if path == root:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        if not _os.path.isfile(path):
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        rest.reverse()
        return path, '/'.join(rest).strip('/')


_open = open


def open(path, mode='r'):
    if 'w' in mode or 'a' in mode:
        raise IOError(
            _errno.EINVAL, path, "Write access not supported")
    elif 'r+' in mode:
        raise IOError(
            _errno.EINVAL, path, "Write access not supported")

    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _open(path, mode)

    else:
        try:
            zf = _zipfile.ZipFile(path, 'r')

        except _zipfile.BadZipFile:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        try:
            data = zf.read(rest)
        except (_zipfile.BadZipFile, KeyError):
            zf.close()
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")
        zf.close()

        if mode == 'rb':
            return _BytesIO(data)

        else:
            if _sys.version_info[0] == 3:
                data = data.decode('ascii')

            return _StringIO(data)


def listdir(path):
    full_path = path
    path, rest = _locate(path)
    if not rest and not _os.path.isfile(path):
        return _os.listdir(path)

    else:
        try:
            zf = _zipfile.ZipFile(path, 'r')

        except _zipfile.BadZipFile:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        result = set()
        seen = False
        try:
            for nm in zf.namelist():
                if rest is None:
                    seen = True
                    value = nm.split('/')[0]
                    if value:
                        result.add(value)

                elif nm.startswith(rest):
                    if nm == rest:
                        seen = True
                        value = ''
                        pass
                    elif nm[len(rest)] == '/':
                        seen = True
                        value = nm[len(rest)+1:].split('/')[0]
                    else:
                        value = None

                    if value:
                        result.add(value)
        except _zipfile.BadZipFile:
            zf.close()
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        zf.close()

        if not seen:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        return list(result)


def isfile(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        ok = _os.path.isfile(path)
        if ok:
            try:
                zf = _zipfile.ZipFile(path, 'r')
                return False
            except (_zipfile.BadZipFile, IOError):
                return True
        return False

    zf = None
    try:
        zf = _zipfile.ZipFile(path, 'r')
        zf.getinfo(rest)
        zf.close()
        return True
    except (KeyError, _zipfile.BadZipFile):
        if zf is not None:
            zf.close()

        # Check if this is a directory
        try:
            zf.getinfo(rest + '/')
        except KeyError:
            pass
        else:
            return False

        rest = rest + '/'
        for nm in zf.namelist():
            if nm.startswith(rest):
                # Directory
                return False

        # No trace in zipfile
        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")


def isdir(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        ok = _os.path.isdir(path)
        if not ok:
            try:
                zf = _zipfile.ZipFile(path, 'r')
            except (_zipfile.BadZipFile, IOError):
                return False
            return True
        return True

    zf = None
    try:
        try:
            zf = _zipfile.ZipFile(path)
        except _zipfile.BadZipFile:
            raise IOError(
                _errno.ENOENT, full_path,
                "No such file or directory")

        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # File found
            return False

        rest = rest + '/'
        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # Directory entry found
            return True

        for nm in zf.namelist():
            if nm.startswith(rest):
                return True

        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")
    finally:
        if zf is not None:
            zf.close()


def islink(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _os.path.islink(path)

    try:
        zf = _zipfile.ZipFile(path)
    except _zipfile.BadZipFile:
        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")
    try:
        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # File
            return False

        rest += '/'
        try:
            zf.getinfo(rest)
        except KeyError:
            pass
        else:
            # Directory
            return False

        for nm in zf.namelist():
            if nm.startswith(rest):
                # Directory without listing
                return False

        raise IOError(
            _errno.ENOENT, full_path,
            "No such file or directory")

    finally:
        zf.close()


def readlink(path):
    full_path = path
    path, rest = _locate(path)
    if rest:
        # No symlinks inside zipfiles
        raise OSError(
            _errno.ENOENT, full_path,
            "No such file or directory")

    return _os.readlink(path)


def getmode(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _stat.S_IMODE(_os.stat(path).st_mode)

    zf = None
    try:
        zf = _zipfile.ZipFile(path)
        info = None

        try:
            info = zf.getinfo(rest)
        except KeyError:
            pass

        if info is None:
            try:
                info = zf.getinfo(rest + '/')
            except KeyError:
                pass

        if info is None:
            rest = rest + '/'
            for nm in zf.namelist():
                if nm.startswith(rest):
                    break
            else:
                raise IOError(
                    _errno.ENOENT, full_path,
                    "No such file or directory")

            # Directory exists, but has no entry of its own.
            return _DFLT_DIR_MODE

        # The mode is stored without file-type in external_attr.
        if (info.external_attr >> 16) != 0:
            return _stat.S_IMODE(info.external_attr >> 16)
        else:
            return _DFLT_FILE_MODE

    finally:
        if zf is not None:
            zf.close()


def getmtime(path):
    full_path = path
    path, rest = _locate(path)
    if not rest:
        return _os.path.getmtime(path)

    zf = None
    try:
        zf = _zipfile.ZipFile(path)
        info = None

        try:
            info = zf.getinfo(rest)
        except KeyError:
            pass

        if info is None:
            try:
                info = zf.getinfo(rest + '/')
            except KeyError:
                pass

        if info is None:
            rest = rest + '/'
            for nm in zf.namelist():
                if nm.startswith(rest):
                    break
            else:
                raise IOError(
                    _errno.ENOENT, full_path,
                    "No such file or directory")

            # Directory exists, but has no entry of its
            # own, fake mtime by using the timestamp of
            # the zipfile itself.
            return _os.path.getmtime(path)

        return _time.mktime(info.date_time + (0, 0, -1))

    finally:
        if zf is not None:
            zf.close()
