#! /usr/bin/env python3

################################################################################
""" Thingy file handling functions

    Copyright (C) 2017-18 John Skilleter

    High-level file access functions not provided by the Python libraries
"""
################################################################################

import os
import shutil
import subprocess

################################################################################

def is_binary_file(filename):
    """Return True if the specified file is likely to be binary.

    Args:
        filename: Path to the file.

    Returns:
        True if the file appears to be binary, otherwise False.
    """

    filetype = file_type(filename, mime=True)

    return filetype.endswith('binary') if filetype else False

################################################################################

def file_type(filename, mime=False):
    """Return a textual description of the file type.

    Args:
        filename: Path to the file.
        mime: If True, request MIME output from `file`.

    Returns:
        Description string, or None if the `file` command failed.

    Raises:
        FileNotFoundError: If the file does not exist or is not readable.
    """

    # The file utility does not return an error if the file does not exist or
    # is not a file, so we have to do that.

    if not os.path.isfile(filename) or not os.access(filename, os.R_OK):
        raise FileNotFoundError('Unable to access %s' % filename)

    cmd = ['file', '--brief']

    if mime:
        cmd.append('--mime')

    cmd.append(filename)

    result = subprocess.run(cmd, capture_output=True, check=False, text=True)

    return result.stdout.split('\n')[0] if result.returncode==0 else None

################################################################################

def format_size(size, always_suffix=False):
    """Convert a byte count to a human-readable string.

    Args:
        size: Size in bytes.
        always_suffix: If True, always include units (e.g. ``" bytes"``).

    Returns:
        Human-readable size string.
    """

    # Keep all the maths positive

    if size < 0:
        size = -size
        sign = '-'
    else:
        sign = ''

    # Default divisor and number of decimal places output

    div = 1

    # Step through the multipliers

    for units in (' bytes' if always_suffix else '', ' KiB', ' MiB', ' GiB', ' TiB'):
        # If we can't scale up to this multiplier quit the loop

        if size // div < 1024:
            break

        # Increase the divisor and set the number of decimal places
        # to 1 (set to 0 when we don't have a divisor).

        div *= 1024
    else:
        units = ' PiB'

    # Calculate the size in tenths so the we get the first digit after
    # the decimal point, doing all the work in the integer domain to
    # avoid rounding errors.

    size_x_10 = (size * 10) // div

    # If the decimal part would be '.0' don't output it

    if size_x_10 % 10 == 0:
        return '%s%d%s' % (sign, size_x_10 // 10, units)

    return '%s%d.%d%s' % (sign, size_x_10 // 10, size_x_10 % 10, units)

################################################################################

def backup(filename, extension='bak', copyfile=True, timestamps=True):
    """Create a backup copy of a file.

    Args:
        filename: Path to the file to back up.
        extension: Extension to use for the backup file (defaults to '.bak').
        copyfile: If True, copy; if False, rename.
        timestamps: If True, preserve timestamps from the original file.

    Returns:
        None.
    """

    # Do nothing if the file does not exist

    if not os.path.isfile(filename):
        return

    # Split on the dot characters

    filename_comp = filename.split('.')

    # Replace the extension with the specified on (or 'bak' by
    # default) or add it if the filename did not have an extension.

    if len(filename_comp) > 1:
        filename_comp[-1] = extension
    else:
        filename_comp.append(extension)

    backupname = '.'.join(filename_comp)

    # Remove any existing backup file

    if os.path.isfile(backupname):
        os.unlink(backupname)

    # Create the backup by copying or renaming the file, optionally preserving the
    # timestamp of the original file

    if timestamps:
        file_info = os.stat(filename)

    if copyfile:
        shutil.copyfile(filename, backupname)
    else:
        os.rename(filename, backupname)

    if timestamps:
        os.utime(backupname, ns=(file_info.st_atime_ns, file_info.st_mtime_ns))

################################################################################
# Test code

if __name__ == "__main__":
    print('Is /bin/sh binary:      %s' % is_binary_file('/bin/sh'))
    print('Is files.py binary:     %s' % is_binary_file('files.py'))
    print('')

    for mimeflag in (False, True):
        print('/bin/sh is:             %s' % file_type('/bin/sh', mimeflag))
        print('/bin/dash is:           %s' % file_type('/bin/dash', mimeflag))
        print('')

    for sizevalue in (0, 1, 999, 1024, 1025, 1.3 * 1024, 2**32 - 1, 2**64 + 2**49):
        print('%24d is %s' % (sizevalue, format_size(sizevalue)))
