#!/usr/bin/env python3
"""
Schedule a reminder

Usage:
    remind [options] <time_spec>...

Options:
    -m <message>, --msg <message>   The message to display

The time spec can either be an absolute time, such as 3:30pm or 15:30, or it can 
be a relative time given in seconds, minutes, or hours. For example:

    remind 4pm
    remind 15m

You can give several time specification, though only one should be absolute, and 
if given, it should be the first.  Multiple relative times accumulate and can be 
negative.  Do not give a negative relative time as the first. So for example:

    remind 4pm -15m

Any argument that is not recognized as a time spec is taken to be a message 
fragment and is appended to the message. So, if you have a doctors appointment 
at 2pm and it will take you 30 minutes to get there and you want to have 
a buffer of 15 minutes, you can use:

    remind 2pm -30m -15m leave for the doctor

At the specified time, in this case 1:15pm, a notification is raised.
"""

# Imports {{{1
from docopt import docopt
from inform import display, fatal, notify, terminate, Color, full_stop
from pathlib import Path
from quantiphy import Quantity, UnitConversion, QuantiPhyError
from time import sleep
import arrow
import os

# Constants {{{1
notification_urgency = 'critical'  # chose from: critical, normal, low
default_message = "It's time!"
__version__ = '1.0.0'
__released__ = '2020-07-19'

# Time conversions {{{1
UnitConversion('s', 'sec second seconds')
UnitConversion('s', 'm min minute minutes', 60)
UnitConversion('s', 'h hr hour hours', 60*60)
UnitConversion('s', 'd day days', 24*60*60)
UnitConversion('s', 'w week weeks', 7*24*60*60)
UnitConversion('s', 'M month months', 30*24*60*60)
UnitConversion('s', 'y year years', 365*24*60*60)
Quantity.set_prefs(ignore_sf=True)

# Time formats {{{1
time_formats = {
    'h:mm:ss A': 'ex. 1:30:00 PM, 1:30:00 pm',
    'h:mm:ssA': 'ex. 1:30:00PM, 1:30:00pm',
    'h:mm A': 'ex. 1:30 PM, 1:30 pm',
    'h:mmA': 'ex. 1:30PM, 1:30pm',
    'hA': 'ex. 1PM or 1pm',
    'hA': 'ex. 1PM or 1pm',
    'HH:mm:ss': 'ex. 13:00:00',
    'HH:mm': 'ex. 13:00',
}
aliases = dict(
    noon = '12pm',
    midnight = '12am',
)

# Utility functions {{{1
def when(seconds):
    if seconds < 120:
        return f'{seconds:0.0f} seconds'
    minutes = seconds / 60
    if minutes < 120:
        return f'{minutes:0.0f} minutes'
    hours = minutes / 60
    return f'{hours:0.1f} hours'

# Main {{{1
cmdline = docopt(
    __doc__,
    version = f'remind {__version__} ({__released__})',
    options_first = True  # need this to allow time offsets to be negative
)

message = []
if cmdline['--msg']:
    message.append(cmdline['--msg'])
seconds = Quantity(0, 's')
now = arrow.now()
target = now
if not Color.isTTY():
    display = notify

# process the command line {{{2
for i, each in enumerate(cmdline['<time_spec>']):
    each = aliases.get(each, each)
    if each == 'now':
        target = arrow.now()
    else:
        for fmt in time_formats:
            try:
                specified = arrow.get(each, fmt)
                delta = specified - specified.floor('day')
                target = now.floor('day') + delta
                if specified < now:
                    target.shift(hours=24)
                break
            except arrow.parser.ParserError:
                pass
            except ValueError as e:
                fatal(full_stop(e), culprit=each)
        else:
            try:
                seconds = seconds.add(Quantity(each, 'm', scale='s'))
            except QuantiPhyError as e:
                message.extend(cmdline['<time_spec>'][i:])
                break

# wait for desired moment {{{2
try:
    target = target.shift(seconds=seconds)
    seconds = (target - now).total_seconds()
    if seconds <= 0:
        seconds += 24*60*60
    if seconds <= 0:
        terminate(f'target time was {target.humanize()}.')
    if not message:
        message = [default_message]
    message = ' '.join(message)
    display(
        f"Alarm scheduled for {target.format('h:mm A')},",
        f"{when(seconds)} from now.",
        codicil = f'Message: {message}',
    )

    # move process to background by duplicating it and exiting the original
    if os.fork():
        os._exit(0)

    # wait
    sleep(seconds)

    # raise the notification
    notify(message, urgency=notification_urgency)

except KeyboardInterrupt:
    display('killed')
