#!/usr/bin/python
import sys
import pwd
import os
import re

def files(dirname):
    try:
        return filter(os.path.isfile, map(lambda f: os.path.join(dirname, f), os.listdir(dirname)))
    except OSError:
        return []

envvar_re = re.compile(r'^([A-Za-z_0-9]+)\s*=\s*(.*)$')

CRONTAB_FILES = ['/etc/crontab'] + files('/etc/cron.d')
ANACRONTAB_FILES = ['/etc/anacrontab']
USERCRONTAB_FILES = files('@statedir@')

TARGER_DIR = sys.argv[1]
TIMERS_DIR = os.path.join(TARGER_DIR, 'cron.target.wants')
SELF = os.path.basename(sys.argv[0])

MINUTES_SET = range(0, 60)
HOURS_SET = range(0, 24)
DAYS_SET = range(0, 32)
DOWS_SET = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
MONTHS_SET = range(0, 13)

ROOT_USER = pwd.getpwnam('root')

try:
    os.makedirs(TIMERS_DIR)
except OSError as e:
    if e.errno != os.errno.EEXIST:
        raise

def parse_crontab(filename, withuser=True, monotonic=False):
    basename = os.path.basename(filename)
    environment = {
        'SHELL': '/bin/sh',
        'PATH': '/usr/bin:/bin',
        }
    with open(filename, 'r') as f:
        for line in f.readlines():
            if line.startswith('#'):
                continue

            line = line.rstrip('\n')
            envvar = envvar_re.match(line)
            if envvar:
                environment[envvar.group(1)] = envvar.group(2)
                continue

            parts = line.split()
            line = ' '.join(parts)

            if monotonic:
                if len(parts) < 4:
                    continue

                period, delay, jobid = parts[0:3]
                command = ' '.join(parts[3:])
                period = {
                        '1': 'daily',
                        '7': 'weekly',
                        '@midnight': 'daily',
                        '30': 'monthly',
                        '31': 'monthly'
                        }.get(period, None) or period.lstrip('@')

                environment['LOGNAME'] = environment['USER'] = 'root'
                environment['HOME'] = ROOT_USER.pw_dir
                yield {
                        'e': ' '.join('"%s=%s"' % kv for kv in environment.iteritems()),
                        's': environment['SHELL'],
                        'l': line,
                        'f': filename,
                        'p': period,
                        'd': delay,
                        'j': jobid,
                        'c': command,
                        'u': 'root'
                        }

            else:
                if line.startswith('@'):
                    if len(parts) < 2:
                        continue

                    period = parts[0]
                    period = {
                            '@midnight': 'daily'
                            }.get(period, None) or period.lstrip('@')

                    user, command = (parts[1], ' '.join(parts[2:])) if withuser else (basename, ' '.join(parts[1:]))

                    environment['LOGNAME'] = environment['USER'] = user
                    environment['HOME'] = pwd.getpwnam(user).pw_dir

                    yield {
                            'e': ' '.join('"%s=%s"' % kv for kv in environment.iteritems()),
                            's': environment['SHELL'],
                            'l': line,
                            'f': filename,
                            'p': period,
                            'u': user,
                            'c': command
                            }
                else:
                    if len(parts) < 6 + int(withuser):
                        continue

                    minutes, hours, days = parts[0:3]
                    months, dows = parts[3:5]
                    user, command = (parts[5], ' '.join(parts[6:])) if withuser else (basename, ' '.join(parts[5:]))

                    environment['LOGNAME'] = environment['USER'] = user
                    environment['HOME'] = pwd.getpwnam(user).pw_dir

                    yield {
                            'e': ' '.join('"%s=%s"' % kv for kv in environment.iteritems()),
                            's': environment['SHELL'],
                            'l': line,
                            'f': filename,
                            'm': parse_time_unit(minutes, MINUTES_SET),
                            'h': parse_time_unit(hours, HOURS_SET),
                            'd': parse_time_unit(days, DAYS_SET),
                            'w': parse_time_unit(dows, DOWS_SET, dow_map),
                            'M': parse_time_unit(months, MONTHS_SET, month_map),
                            'u': user,
                            'c': command
                            }

def parse_time_unit(value, values, mapping=int):
    if value == '*':
        return ['*']
    return sorted(list(reduce(lambda a, i: a.union(set(i)), map(values.__getitem__,
        map(parse_period(mapping), value.split(','))), set())))

def month_map(month):
    try:
        return int(month)
    except ValueError:
        return ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'nov', 'dec'].index(month.lower()[0:3]) + 1

def dow_map(dow):
    try:
        return ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].index(dow[0:3].lower())
    except ValueError:
        return int(dow) % 7

def parse_period(mapping=int):
    def parser(value):
        try:
            range, step = value.split('/')
        except ValueError:
            value = mapping(value)
            return slice(value, value + 1)

        if range == '*':
            return slice(None, None, int(step))

        try:
            start, end = range.split('-')
        except ValueError:
            return slice(mapping(range), None, int(step))

        return slice(mapping(start), mapping(end), int(step))

    return parser

def generate_timer_unit(job, seq):
    n = next(seq)
    unit_name = "cron-%s-%s" % (job['u'], n)

    persistent='@persistent@'

    if 'p' in job:
        if job['p'] == 'reboot':
            schedule = 'OnBootSec=%sm' % job.get('d', 5)
        else:
            try:
                schedule = 'OnCalendar=*-*-1/%s 0:%s:0' % (int(job['p']), job.get('d', 0))
            except ValueError:
                schedule = 'OnCalendar=%s' % job['p']

        accuracy = job.get('d', 1)

    else:
        dows = ','.join(job['w'])
        dows = '' if dows == '*' else dows + ' '

        schedule = 'OnCalendar=%s*-%s-%s %s:%s:00' % (dows, ','.join(map(str, job['M'])),
                ','.join(map(str, job['d'])), ','.join(map(str, job['h'])), ','.join(map(str, job['m'])))
        accuracy = 1

    if 'p' in job and job['p'] == "hourly":
	persistent=''

    if ('p' not in job and
       job['M'] == ['*'] and
       job['w'] == ['*'] and
       job['d'] == ['*'] and
       job['h'] == ['*']):
	persistent=''

    with open('%s/%s.timer' % (TARGER_DIR, unit_name), 'w') as f:
        f.write('''# Automatically generated by %s

[Unit]
Description=[Cron] "%s"
PartOf=cron.target
RefuseManualStart=true
RefuseManualStop=true
SourcePath=%s

[Timer]
Unit=%s.service
%s
#AccuracySec=%sm
%s
''' % (SELF, job['l'], job['f'], unit_name, schedule, accuracy, persistent))

    try:
        os.symlink('%s/%s.timer' % (TARGER_DIR, unit_name), '%s/%s.timer' % (TIMERS_DIR, unit_name))
    except OSError as e:
        if e.errno != os.errno.EEXIST:
            raise

    with open('%s/%s.service' % (TARGER_DIR, unit_name), 'w') as f:
        f.write('''# Automatically generated by %s

[Unit]
Description=[Cron] "%s"
RefuseManualStart=true
RefuseManualStop=true
SourcePath=%s

[Service]
Type=oneshot
User=%s
Environment=%s
ExecStart=%s -c '%s'
''' % (SELF, job['l'], job['f'], job['u'], job['e'], job['s'], job['c']))

    return '%s.timer' % unit_name


seqs = {}
def count():
    n = 0
    while True:
        yield n
        n += 1


for filename in CRONTAB_FILES:
    try:
        for job in parse_crontab(filename, withuser=True):
            generate_timer_unit(job, seqs.setdefault(job['u'], count()))
    except IOError:
        pass

for filename in ANACRONTAB_FILES:
    try:
        for job in parse_crontab(filename, monotonic=True):
            generate_timer_unit(job, seqs.setdefault(job['u'], count()))
    except IOError:
        pass

for filename in USERCRONTAB_FILES:
    try:
        for job in parse_crontab(filename, withuser=False):
            generate_timer_unit(job, seqs.setdefault(job['u'], count()))
    except IOError:
        pass

