import os
import sys
import re

import tarfile
import rarfile
from py7zr import SevenZipFile
from zipfile import ZipFile
from rarfile import RarFile
from tarfile import TarFile


SUPPORTED_ARCHIVE = [
    '.docx',
    '.egg',
    '.jar',
    '.odg',
    '.odp',
    '.ods',
    '.odt',
    '.pptx',
    '.tar',
    '.tar.bz2',
    '.tar.gz',
    '.tgz',
    '.tz2',
    '.xlsx',
    '.zip',
    '.rar',
    '.7z',
    'tar.xz',
]

TAR_ARCHIVE = [
    '.tar',
    '.tar.bz2',
    '.tar.gz',
    'tar.xz',
    '.tgz',
    '.tz2',
]
IS_PY2 = sys.version_info[0] == 1


if IS_PY2:
    def is_string(obj):
        return isinstance(obj, basestring)
else:
    def is_string(obj):
        return isinstance(obj, str)


class ArchiveException(Exception):
    """Base exception class for all archive errors."""


class UnrecognizedArchiveFormat(ArchiveException):
    """Error raised when passed file is not a recognized archive format."""


class UnsafeArchive(ArchiveException):
    """
    Error raised when passed file contains paths that would be extracted
    outside of the target directory.
    """


def extract(path, to_path='', ext='', **kwargs):
    """
    Unpack the tar or zip file at the specified path to the directory
    specified by to_path.
    """
    Archive(path, ext=ext).extract(to_path, **kwargs)


class Archive(object):
    """
    The external API class that encapsulates an archive implementation.
    """

    def __init__(self, file, ext=''):
        """
        Arguments:
        * 'file' can be a string path to a file or a file-like object.
        * Optional 'ext' argument can be given to override the file-type
          guess that is normally performed using the file extension of the
          given 'file'.  Should start with a dot, e.g. '.tar.gz'.
        """
        self._archive = self._archive_cls(file, ext=ext)(file)
        self.path = file

    @staticmethod
    def _archive_cls(file, ext=''):
        """
        Return the proper Archive implementation class, based on the file type.
        """
        cls = None
        filename = None
        if is_string(file):
            filename = file
        else:
            try:
                filename = file.name
            except AttributeError:
                raise UnrecognizedArchiveFormat(
                    "File object not a recognized archive format.")
        lookup_filename = filename + ext
        base, tail_ext = os.path.splitext(lookup_filename.lower())
        cls = extension_map.get(tail_ext)
        if not cls:
            base, ext = os.path.splitext(base)
            cls = extension_map.get(ext)

        if not cls:
            raise UnrecognizedArchiveFormat(
                "Path not a recognized archive format: %s" % filename)
        return cls

    def extract(self, *args, **kwargs):
        self._archive.extract(*args, **kwargs)

    def list(self):
        return self._archive.list()

    def namelist(self):
        return self._archive.namelist()

    def close(self):
        return self._archive.close()

    def is_encrypted(self):
        return self._archive.is_encrypted()

    def extractall(self, path_from_local='', requested_file=''):
        files = []
        requested_file = requested_file if requested_file != '' else self.path
        file_path = os.path.join(path_from_local, requested_file)
        destination_path = path_from_local \
            if path_from_local != '' else os.path.dirname(file_path)

        try:
            parent_archive = Archive(file_path)
            parent_archive.extract(destination_path)
            namelist = parent_archive.namelist()
            parent_archive.close()

            for name in namelist:
                if os.path.splitext(name)[-1].lower() in SUPPORTED_ARCHIVE:
                    self.extractall(
                        requested_file=name,
                        path_from_local=destination_path
                    )

                else:
                    files.append(name)
        except Exception as e:
            print(e)
            pass
        return destination_path


class BaseArchive(object):
    """
    Base Archive class.  Implementations should inherit this class.
    """
    def __del__(self):
        if hasattr(self, "_archive"):
            if isinstance(self._archive, SevenZipFile):
                pass
            else:
                self._archive.close()

    def list(self):
        raise NotImplementedError()

    def filenames(self):
        """
        Return a list of the filenames contained in the archive.
        """
        raise NotImplementedError()

    def _extract(self, to_path):
        """
        Performs the actual extraction.  Separate from 'extract' method so that
        we don't recurse when subclasses don't declare their own 'extract'
        method.
        """
        self._archive.extractall(to_path)

    def extract(self, to_path='', method='safe'):
        if method == 'safe':
            self.check_files(to_path)
        elif method == 'insecure':
            pass
        else:
            raise ValueError("Invalid method option")
        self._extract(to_path)

    def check_files(self, to_path=None):
        """
        Check that all of the files contained in the archive are within the
        target directory.
        """
        if to_path:
            target_path = os.path.normpath(os.path.realpath(to_path))
        else:
            target_path = os.getcwd()
        for filename in self.filenames():
            extract_path = os.path.join(target_path, filename)
            extract_path = os.path.normpath(os.path.realpath(extract_path))
            if not extract_path.startswith(target_path):
                raise UnsafeArchive(
                    "Archive member destination is outside the target"
                    " directory.  member: %s" % filename)


class TarArchive(BaseArchive):

    def __init__(self, file):
        # tarfile's open uses different parameters for file path vs. file obj.
        if is_string(file):
            self._archive = tarfile.open(name=file)
        else:
            self._archive = tarfile.open(fileobj=file)

    def list(self, *args, **kwargs):
        self._archive.list(*args, **kwargs)

    def namelist(self):
        return self._archive.getnames()

    def filenames(self):
        return self._archive.getnames()

    def is_encrypted(self):
        return False

    def close(self):
        return self._archive.close()

    def extractall(self, file):
        return self._archive.extractall(file)


class SevenZipArchive(BaseArchive):

    def __init__(self, file):
        self._archive = SevenZipFile(file, mode='r')

    def list(self, *args, **kwargs):
        self._archive.printdir(*args, **kwargs)

    def filenames(self):
        return self._archive.getnames()

    def namelist(self):
        return self._archive.getnames()

    def close(self):
        try:
            return self._archive.close()
        except Exception as e:
            print(e)

    def extract(self, file):
        return self._archive.extract(file)

    def is_encrypted(self):
        return self._archive.password_protected

    def extractall(self, file):
        return self._archive.extractall(file)

#    def printdir(self, file=None):
#        """Print a table of contents for the zip file."""
#        print("%-46s %19s %12s" % ("File Name", "Modified    ", "Size"),
#              file=file)
#        for file_ in self._archive.files:
#            date = "%d-%02d-%02d %02d:%02d:%02d" % file_.date_time[:6]
#            print("%-46s %s %12d" % (file_['filename'], date, file_.file_size),
#                  file=file)


class ZipArchive(BaseArchive):

    def __init__(self, file):
        self._archive = ZipFile(file)

    def list(self, *args, **kwargs):
        self._archive.printdir(*args, **kwargs)

    def filenames(self):
        return self._archive.namelist()

    def namelist(self):
        return self._archive.namelist()

    def close(self):
        return self._archive.close()

    def is_encrypted(self):
        for file_ in self._archive.infolist():
            if file_.flag_bits & 0x1 != 0:
                return True
            else:
                return False

    def extractall(self, file):
        return self._archive.extractall(file)


class RarArchive(BaseArchive):

    def __init__(self, file):
        self._archive = RarFile(file)

    def namelist(self):
        return self._archive.namelist()

    def filenames(self):
        return self._archive.namelist()

    def close(self):
        return self._archive.close()

    def is_encrypted(self):
        return self._archive.needs_password()

    def extraxt(self):
        rarfile = rarfile.RarFile.self._archive
        rarfile.extraxt(self._archive._rarfile)

    def extractall(self, file):
        return self._archive.extractall(file)

    def list(self, *args, **kwargs):
        self.printdir(*args, **kwargs)

    def printdir(self, file=None):
        """Print a table of contents for the zip file."""
        print("%-46s %19s %12s" % ("File Name", "Modified    ", "Size"),
              file=file)
        for file_ in self._archive._file_parser.infolist():
            date = "%d-%02d-%02d %02d:%02d:%02d" % file_.date_time[:6]
            print("%-46s %s %12d" % (file_.filename, date, file_.file_size),
                  file=file)

extension_map = {
    '.docx': ZipArchive,
    '.egg': ZipArchive,
    '.jar': ZipArchive,
    '.odg': ZipArchive,
    '.odp': ZipArchive,
    '.ods': ZipArchive,
    '.odt': ZipArchive,
    '.pptx': ZipArchive,
    '.tar': TarArchive,
    '.tar.bz2': TarArchive,
    '.tar.gz': TarArchive,
    '.tgz': TarArchive,
    '.tz2': TarArchive,
    '.xlsx': ZipArchive,
    '.zip': ZipArchive,
    '.rar': RarArchive,
    '.7z': SevenZipArchive,
}

