#!/usr/bin/python3

'''Recreate a valid block stream, and give progress updates'''

# pylint: disable=wrong-import-position

import os
import re
import sys
import time
import select
import signal

sys.path.append('/usr/local/lib')

import modunits  # nopep8

signal.signal(signal.SIGPIPE, signal.SIG_DFL)


def usage(retval):
    '''Give a usage message'''
    if retval:
        write = sys.stderr.write
    else:
        write = sys.stdout.write

    write('usage: {} [-v] [-e size_in_bytes] [-b blocksize_in_bytes] [-t timeout_in_seconds] [-p]\n'.format(sys.argv[0]))
    write('    -v means verbose\n')
    write('    -e provides a size estimate, for when we cannot stat the file length (EG in a pipe)\n')
    write('    -b provides a block size in bytes\n')
    write('    -t provides a timeout duration in seconds\n')
    write('    -p says to pad the final block with nulls\n')
    write('\n')
    write('    size_in_bytes defaults to {}\n'.format(State.estimate_in_bytes))
    write('    blocksize defaults to {}\n'.format(State.blocksize))
    write('    timeout defaults to {}\n'.format(State.timeout))

    sys.exit(retval)


def percent(numerator, denominator):
    '''Compute a percentage as a string for eye-pleasing'''

    quotient = (numerator * 100.0) / denominator
    str_quotient = str(quotient)
    match_obj = re.match(r'^([0-9]+\.[0-9]).*$', str_quotient)
    return match_obj.group(1)


def compute_eta_and_bps(size_now, final_size, initial_time, time_now):
    '''
    Compute the Estimated Time of Arrival for the transfer:
    IOW, when the transfer is (was) expected to complete.
    '''

    delta_time_now = time_now - initial_time

    if size_now <= 0.0:
        abs_secs = 'NaN'
    else:
        delta_seconds_at_completion = (final_size * delta_time_now) / size_now

        absolute_seconds_at_completion = initial_time + delta_seconds_at_completion

        abs_secs = absolute_seconds_at_completion

    if delta_time_now <= 0:
        bps = 'NaN'
    else:
        bytes_per_second = size_now / (time_now - initial_time)

        bps = modunits.modunits(
            'computer-bits-per-second-si',
            bytes_per_second * 8,
            fractional_part_length=1,
            units='abbreviated',
            )

    return time.ctime(abs_secs), bps


class State(object):
    '''Hold transfer state, including command line option results'''
    verbose = False
    estimate_in_bytes = -1
    blocksize = 2 ** 18
    timeout = 180
    pad = False
    stdin = sys.stdin.fileno()
    stdout = sys.stdout.fileno()

    def __init__(self, argv):
        while argv[1:]:
            if argv[1] == '-v':
                State.verbose = True
            elif argv[1] == '-p':
                State.pad = True
            elif argv[1] == '-e':
                State.estimate_in_bytes = int(argv[2])
                del argv[2]
            elif argv[1] == '-b':
                State.blocksize = int(argv[2])
                del argv[2]
            elif argv[1] == '-t':
                State.timeout = float(argv[2])
                del argv[2]
            elif argv[1] in ['-h', '--help']:
                usage(0)
            else:
                sys.stderr.write('Unrecognized option: {}\n'.format(argv[1]))
                usage(1)
            del argv[1]

        self.file_size = -1
        self.major_buffer = b''
        self.minor_buffer = b''

    def set_file_size(self):
        '''Decide the size of the file - in bytes'''

        try:
            self.file_size = os.fstat(State.stdin).st_size
        except OSError:
            self.file_size = self.estimate_in_bytes
        else:
            if self.file_size == 0:
                self.file_size = self.estimate_in_bytes

        assert self.file_size >= 0, "Was unable to find length of file via fstat or -e"

    @classmethod
    def input_on_stdin(cls):
        '''Return True if there is input on stdin, False if there is a timeout'''
        select_result = select.select([cls.stdin], [], [], cls.timeout)
        assert not select_result[1]
        assert not select_result[2]
        if not select_result[0]:
            # This is a timeout, return False
            return False
        # This is a stdin-ready condition, return True
        return True

    def run(self):
        '''Copy blocks from State.stdin to State.stdout'''

        blockno = 0
        size_so_far = 0
        initial_time = time.time()
        while True:
            if self.input_on_stdin():
                self.minor_buffer = os.read(State.stdin, State.blocksize)
                if not self.minor_buffer:
                    # EOF, write final block and exit program
                    self.finish()
                    sys.exit(0)
                size_so_far += len(self.minor_buffer)
                eta_time, bps = compute_eta_and_bps(size_so_far, self.file_size, initial_time, time.time())
                sys.stderr.write('blockno {}: {}% of {}, ETA: {}, Rate: {}\n'.format(
                    blockno,
                    percent(size_so_far, self.file_size),
                    modunits.modunits('computer-size-si', self.file_size),
                    eta_time,
                    bps,
                    ))
                self.major_buffer += self.minor_buffer
                if len(self.major_buffer) >= State.blocksize:
                    portion_to_write = self.major_buffer[:State.blocksize]
                    self.major_buffer = self.major_buffer[State.blocksize:]
                    os.write(State.stdout, portion_to_write)
                blockno += 1
            else:
                # We hit a timeout, finish and exit shell-false
                sys.stderr.write('{}: timeout!\n'.format(sys.argv[0]))
                self.finish()
                sys.exit(1)

    def finish(self):
        '''Write the last block, possibly padded to a full block length'''

        if self.pad:
            if self.major_buffer:
                if self.verbose:
                    sys.stderr.write('Padding final block\n')
                final_long_block = self.major_buffer + b'\0' * State.blocksize
                final_block = final_long_block[:State.blocksize]
                os.write(State.stdout, final_block)
            else:
                # major_buffer is empty, nothing to do
                if self.verbose:
                    sys.stderr.write('Final block was whole\n')
        else:
            if self.verbose:
                sys.stderr.write('Final block not being padded by request of user\n')
            os.write(State.stdout, self.major_buffer)


def main():
    '''Main function'''
    state = State(sys.argv)
    state.set_file_size()
    state.run()


main()