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

import errno as _errno
import os as _os
import stat as _stat
import sys as _sys
import time as _time
import zipfile as _zipfile

_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 _BaseBytesIO
    from StringIO import StringIO as _BaseStringIO

    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 BytesIO as _BytesIO
    from io import StringIO as _StringIO


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.error:
            raise IOError(_errno.ENOENT, full_path, "No such file or directory")

        try:
            data = zf.read(rest)
        except (_zipfile.error, 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.error:
            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]  # noqa: E203
                    else:
                        value = None

                    if value:
                        result.add(value)
        except _zipfile.error:
            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.error, IOError):
                return True
        return False

    zf = None
    try:
        zf = _zipfile.ZipFile(path, "r")
        zf.getinfo(rest)
        zf.close()
        return True
    except (KeyError, _zipfile.error):
        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.error, IOError):
                return False
            return True
        return True

    zf = None
    try:
        try:
            zf = _zipfile.ZipFile(path)
        except _zipfile.error:
            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.error:
        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()
