#!/usr/bin/env python3
"""A simple countdown timer.

Times can be specified in the following formats:

   - S where S > 0
   - M:S where M > 0 and S in [00, 59]
   - H:M:S where H > 0 and M in [00, 59] and S in [00, 59]

"""
import argparse
import datetime
import re
import sys
import time


DEFAULT_MESSAGE = 'Elapsed: {elapsed} Remaining: {remaining}'
S_RE = r'^(?P<seconds>[1-9]\d*)$'
MS_RE = r'^(?P<minutes>[1-9]\d*):(?P<seconds>[0-5]\d)$'
HMS_RE = r'^(?P<hours>[1-9]\d*):(?P<minutes>[0-5]\d):(?P<seconds>[0-5]\d)$'


def seconds_from_str(arg):
    for pattern in (S_RE, MS_RE, HMS_RE):
        match = re.search(pattern, arg)
        if match is not None:
            break
    else:
        raise ValueError(arg)
    d = match.groupdict()
    h, m, s = d.get('hours', 0), d.get('minutes', 0), d.get('seconds')
    h, m, s = int(h), int(m), int(s)
    return h, m, s


def get_hours_minutes_and_seconds(seconds):
    h, s = divmod(seconds, 3600)
    m, s = divmod(s, 60)
    h, m, s = int(h), int(m), int(round(s))
    return h, m, s


def format_time(seconds_):
    hours, minutes, seconds = get_hours_minutes_and_seconds(seconds_)
    return ':'.join('{:0>2}'.format(p) for p in (hours, minutes, seconds))


def format_datetime(dt):
    return dt.strftime('%H:%M:%S')


def print_message(message, elapsed, remaining, end=''):
    format_args = {
        'elapsed': format_time(elapsed),
        'remaining': format_time(remaining),
    }
    print('\r', ' ' * 80, '\r', message.format(**format_args), sep='', end=end, flush=True)


def run(time_tuple, message=DEFAULT_MESSAGE, end_at=False, repeat=0, quiet=False, verbose=False, *,
        _repeats=0):
    if quiet and verbose:
        raise ValueError('Cannot specify quiet and verbose at the same time')

    start_datetime = datetime.datetime.now()
    h, m, s = time_tuple

    if end_at:
        expected_end_time = datetime.time(hour=h, minute=m, second=s)
        expected_end_datetime = datetime.datetime.combine(start_datetime.date(), expected_end_time)
    else:
        delta = datetime.timedelta(hours=h, minutes=m, seconds=s)
        expected_end_datetime = start_datetime + delta

    seconds = (expected_end_datetime - start_datetime).total_seconds()
    seconds = int(round(seconds))

    start_time = time.perf_counter()

    if verbose:
        print('Started:', format_datetime(start_datetime))
        print('Ends at:', format_datetime(expected_end_datetime))

    for elapsed in range(seconds):
        if not quiet:
            print_message(message, elapsed, seconds - elapsed)
        # Sleep for ~1 second, adjusting for skew.
        sleep_time = 1 - ((time.perf_counter() - start_time) - elapsed)
        # When the calculated sleep time is less than zero, that
        # indicates that the process has been suspended or slept for
        # longer than expected (longer than 1 second); attempt to "catch
        # up" by not sleeping.
        if sleep_time > 0:
            time.sleep(sleep_time)

    end_time = time.perf_counter()
    real_elapsed = end_time - start_time
    real_remaining = seconds - real_elapsed

    if round(real_remaining) < 0:
        err = '\rError: Timer overran by {:.3f} seconds'.format(-real_remaining)
        print(err, file=sys.stderr)
    elif not quiet:
        print_message(message, real_elapsed, 0, end='\n')

    end_datetime = datetime.datetime.now()

    if verbose:
        print('Stopped:', format_datetime(end_datetime))

    if abs(end_datetime - expected_end_datetime) > datetime.timedelta(seconds=1):
        print('Expected:', format_datetime(expected_end_datetime), file=sys.stderr)

    if _repeats < repeat:
        if not quiet:
            print('=' * 80)
        run(seconds, message, repeat - 1, quiet, verbose, _repeats=_repeats + 1)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
            description=__doc__,
            formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        'time', type=seconds_from_str,
        help='[[H:]M:]S')
    parser.add_argument(
        '-e', '--end-at', action='store_true', default=False,
        help='End at the specified time')
    parser.add_argument(
        '-r', '--repeat', type=int, default=0,
        help='Repeat N times')
    parser.add_argument(
        '--message', default=DEFAULT_MESSAGE,
        help='Message to display, updated every second; can include format strings: '
             '{elapsed}, {remaining} ["%s"]' % DEFAULT_MESSAGE)

    verbosity_group = parser.add_mutually_exclusive_group()
    verbosity_group.add_argument(
        '-q', '--quiet', action='store_true', default=False,
        help='Print nothing')
    verbosity_group.add_argument(
        '-v', '--verbose', action='store_true', default=False,
        help='Print additional info')

    args = parser.parse_args()
    try:
        run(args.time, args.message, args.end_at, args.repeat, args.quiet, args.verbose)
    except KeyboardInterrupt:
        if not args.quiet:
            print_message('Aborted\n', 0, 0)
