"""
Some simple functions for dealing with gzipped files
"""
import binascii
import gzip
import hashlib


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def gzip_agnostic(fh, mode='t', **kwargs):
    """
    Checks to see if a file is a gzip file and opens the correct file handle
    for gzipped files. It can either accept a file handle or file name as the
    argument. This is an attempt at a gzipped agnostic file opener

    Parameters
    ----------
    fh : :obj:`FileObject` or `str`
        Either the file name or the file object to test
    mode : str
        All the modes are read modes, so the choice here is `t` or `b`
    **kwargs
        Any other arguments such as encoding
    """

    # If the argument is not a file handle then attempt to open it
    try:
        # If we try to open and get a TypeError, we are assuming it is a file
        # like object already
        fh = open(fh, 'rb')
    except TypeError:
        pass

    # The file may be from STDIN r a file descriptor, we can test this
    # by attempting a seek, if this fails with an illegal seek then
    # there is not much we can do so we ust return the file object as if
    # we start to read it and it is not gzipped then we can't move
    # back to the beginning
    try:
        fh.tell()
    except IOError as e:
        if str(e) == "[Errno 29] Illegal seek":
            return fh
        # If anything else goes wrong then we raise it
        raise

    # We read the first two bytes and we will look at their values
    # to see if they are the gzip characters
    testchr = fh.read(2)

    # Test for the gzipped characters
    if binascii.b2a_hex(testchr).decode() == "1f8b":
        # If it is gziped, then close it and reopen as a gzipped file
        fn = fh.name
        fh.close()
        fh = gzip.open(fn, 'r{0}'.format(mode), **kwargs)
    else:
        # If not seek back to the position that we started
        fn = fh.name
        fh.close()
        fh = open(fn, 'r{0}'.format(mode), **kwargs)

    # Return the opened file
    return fh


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def gzip_fh(fh):
    """
    Checks to see if a file is a gzip file and opens the correct file handle
    for gzipped files. It can either accept a file handle or file name as the
    argument. This is an attempt at a gzipped agnostic file opener

    Parameters
    ----------
    fh : :obj:`FileObject` or `str`
        Either the file name or the file object to test
    """
    return gzip_agnostic(fh)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def is_gzip(fh):
    """
    Determine if a file is gzipped by looking at the first few bytes in the
    file

    Parameters
    ----------

    fh : :obj:`str` or `FileObject`
        A route to the file that needs checking
    """

    # If the argument is not a file handle then attempt to open it
    try:
        # If we try to open and get a TypeError, we are assuming it is a file
        # like object already
        fh = open(fh, 'rb')
    except TypeError:
        pass

    # The file may be from STDIN r a file descriptor, we can test this
    # by attempting a seek, if this fails with an illegal seek then
    # there is not much we can do so we ust return the file object as if
    # we start to read it and it is not gzipped then we can't move
    # back to the beginning
    try:
        fh.tell()
    except IOError as e:
        if str(e) != "[Errno 29] Illegal seek":
            # If anything else goes wrong then we raise it
            raise

    # We read the first two bytes and we will look at their values
    # to see if they are the gzip characters
    testchr = fh.read(2)
    is_gzipped = False

    # Test for the gzipped characters
    if binascii.b2a_hex(testchr).decode() == "1f8b":
        # If it is gziped, then close it and reopen as a gzipped file
        is_gzipped = True

    fh.close()
    return is_gzipped


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def hash_for_file(path, algorithm='md5',
                  block_size=256*128, human_readable=True):
    """
    Block size directly depends on the block size of your filesystem
    to avoid performances issues
    Here I have blocks of 4096 octets (Default NTFS)

    Linux Ext4 block size
    sudo tune2fs -l /dev/sda5 | grep -i 'block size'
    > Block size:               4096

    Input:
        path: a path
        algorithm: an algorithm in hashlib.algorithms
                   ATM: ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512')
        block_size: a multiple of 128 corresponding to the block size of your
                    filesystem
        human_readable: switch between digest() or hexdigest() output, default
                         hexdigest()
    Output:
        hash
    """
    # Make sure we have the algorithm
    if algorithm not in hashlib.algorithms_available:
        raise NameError('The algorithm "{algorithm}" you specified is '
                        'not a member of "hashlib.algorithms"'
                        .format(algorithm=algorithm))

    # According to hashlib documentation using new()
    # will be slower then calling using named
    # constructors, ex.: hashlib.md5()
    hash_algo = hashlib.new(algorithm)

    # This does the file line by line so can handle big files
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(block_size), b''):
            hash_algo.update(chunk)

    # Return the hex if requeired
    if human_readable:
        file_hash = hash_algo.hexdigest()
    else:
        file_hash = hash_algo.digest()

    return file_hash
