#!/usr/local/bin/python3

import sys, datetime


def ignore_cron_line(s):
    return not (len(s) == 0 or
                s.startswith("#") or
                s.startswith("MAILTO") or
                s.startswith("HOME") or
                s.startswith("SHELL") or
                s.startswith("PATH"))

def strip_leading_zeroes(s):
    while len(s) > 1 and s[0] == "0":
        s = s[1:]
    return s

def explode(field, allowed):
    exploded = []

    for part in field.split(","):
        step = part.split("/", 1)

        if len(step) == 1:
            step += ("1",)
        elif not step[1].isnumeric() or int(step[1]) <= 0:
            return None

        if step[0] == "*":
            allowed_values = sorted(set(allowed.values()))
            exploded += range(allowed_values[0], allowed_values[-1] + 1,
                              int(step[1]))
            continue

        rang = list(map(strip_leading_zeroes, step[0].split("-", 1)))
        if not rang[0] in allowed:
            return None

        if len(rang) == 1:
            rang += (rang[0],)
        elif not rang[1] in allowed:
            return None

        exploded += range(allowed[rang[0]], allowed[rang[1]] + 1,
                          int(step[1]))

    return sorted(set(exploded))


maxprint = 20

if len(sys.argv) > 1:
    if "-h" in sys.argv[1] or "--help" in sys.argv[1]:
        print("""nextcron.py -- displays upcoming cronjobs
Usage:
  crontab -l | nextcron.py [<maxjobs>]
  nextcron.py [<maxjobs>] < /etc/crontab
  cat /var/cron/tabs/* | nextcron.py [<maxjobs>]

Parameters:
  <maxjobs>     Display just next <maxjobs> upcoming cronjobs; default: 20
  -h, --help    Show this help ;-)""")
        sys.exit()

    if sys.argv[1].isnumeric():
        maxprint = int(sys.argv[1])

ALLOWED = (
  ("minute",
   {str(x): x for x in range(0, 60)}),
  ("hour",
   {str(x): x for x in range(0, 24)}),
  ("day",
   {str(x): x for x in range(1, 32)}),
  ("month",
   {**{str(x): x for x in range(1, 13)},
    **{"Jan": 1, "Feb": 2, "Mar": 3, "Apr":  4, "May":  5, "Jun":  6,
       "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12}}),
  ("weekday",
   {**{str(x): x-1 for x in range(1, 8)},
    **{"Sun": 6, "Mon": 0, "Tue": 1, "Wed": 2,
       "Thu": 3, "Fri": 4, "Sat": 5, "0": 6}})
)
SPECIAL = {
  "@yearly":   ("0", "0", "1", "1", "*"),
  "@annually": ("0", "0", "1", "1", "*"),
  "@monthly":  ("0", "0", "1", "*", "*"),
  "@weekly":   ("0", "0", "*", "*", "0"),
  "@daily":    ("0", "0", "*", "*", "*"),
  "@midnight": ("0", "0", "*", "*", "*"),
  "@hourly":   ("0", "*", "*", "*", "*")
}

cronjobs = []

for line in filter(ignore_cron_line, map(lambda l: l.rstrip(), sys.stdin)):
    if line.startswith("@"):
        fields = line.split(maxsplit=1)
        if len(fields) != 2:
            sys.exit("Invalid crontab entry: %s" % (line,))

        if fields[0] == "@reboot" or fields[0] == "@noauto":
            print("Ignoring %s" % (line,), file=sys.stderr)
            continue
        elif fields[0] in SPECIAL:
            fields = SPECIAL[fields[0]]
        else:
            sys.exit("Invalid time specification: %s" % (line,))
    else:
        fields = line.split(maxsplit=5)
        if len(fields) != 6:
            sys.exit("Invalid crontab entry: %s" % (line,))

    job = {"line": line,
           "weekday_and_day": (not fields[2].startswith("*") and
                               not fields[4].startswith("*"))}

    for i, (fieldname, allowed) in enumerate(ALLOWED):
        job[fieldname] = explode(fields[i], allowed)
        if job[fieldname] is None:
            sys.exit("Invalid %s field: %s" % (fieldname, line))
        
    cronjobs.append(job)

if len(cronjobs) == 0:
    sys.exit()

now = datetime.datetime.now().replace(second=0).replace(microsecond=0)
printed = 0

while printed < maxprint:
    for job in cronjobs:
        if job["weekday_and_day"]:
           day_match = (now.day       in job["day"] or
                        now.weekday() in job["weekday"])
        else:
           day_match = (now.day       in job["day"] and
                        now.weekday() in job["weekday"])

        if (now.minute in job["minute"] and
            now.hour   in job["hour"]   and
            day_match                   and
            now.month  in job["month"]):
            print(now, ":", job["line"])
            printed += 1
            if printed >= maxprint:
                sys.exit()
    now += datetime.timedelta(minutes=1)
