#!/usr/bin/env python2
"""
Tool that creates batches of iCal (.ics) files for festivals or congresses.
Try running icsmaker.py without options to get help.
Try running icsmaker.py against itself to generate the following example:
\begin

    # first, let's set up a location to write into all events (optional)
    \location World's End

    # then, we need to specify a date (mandatory)
    \date 2012-12-21

    13:37 this is our first simple event
    11:10-13:15 this event has a beginning and an end
    13:20 - 14:12 whitespace between times doesn't matter, by the way
    16:00 +1:00 this event takes exactly one hour
    17:00 this event contains a description reference in angle brackets <foo>

    # our main event may of course span multiple days
    \date 2012-12-22
    0:00 See, we're still alive!

    \text foo
    In such a block, you can write a full description of an event, which can
    span multiple lines. Note that formatting is taken verbatim, including
    line breaks and indentation, so be careful with how you write your text.
    By the way, all times are in the local timezone.
    \endtext

\end
By default, multiple .ics files (one for every sub-event) will be generated.
Using the '-single' option, all events are packed into a single .ics file.
Alarms can be added with the '-alarm' option.
"""
import sys, re, os, time

re_command = re.compile(r'\\(\w+)(\s+(.*))?')
re_event = re.compile(r'(\d\d?):(\d\d)(\s*([+-])\s*(\d\d?):(\d\d))?\s+(.*)')
re_textref = re.compile(r'(.*?)\s+<(\w+)>$')

PRODID = "icsmaker"

class Events:
    def __init__(self):
        self.outside = False
        self.location = None
        self.events = []
        self.text = None
        self.texts = {}
        self.alarm = None
        self.tzadj = 0

    def mktime(self, m, s):
        return time.mktime((self.y, self.m, self.d, m, s, 0, 0,0,-1)) + self.tzadj

    def cmd_location(self, loc):
        self.location = loc

    def cmd_date(self, date):
        self.y, self.m, self.d = map(int, date.split('-'))

    def cmd_text(self, name):
        self.text = name
        self.texts[name] = ""

    def cmd_endtext(self, dummy=None):
        self.texts[self.text] = self.texts[self.text].strip()
        self.text = None

    def cmd_end(self, dummy):
        self.outside = True

    def parse_line(self, line):
        if self.outside:
            self.outside = not(line.strip().startswith("\\begin"))
            return
        if self.text and line.strip().startswith("\\text"):
            self.cmd_endtext(self)
        if self.text and not(line.strip().startswith("\\end")):
            self.texts[self.text] += "\n" + line
            return
        line = line.split('#', 1)[0].strip()
        if not line: return
        if line.startswith('"""'):
            self.outside = True
            return

        # check for command
        m = re_command.match(line)
        if m:
            try:
                proc = getattr(self, "cmd_" + m.group(1))
            except AttributeError:
                print "unknown command \\" + m.group(1)
                return
            return proc(m.group(3))

        # check for event
        m = re_event.match(line)
        if m:
            dtstart = self.mktime(int(m.group(1)), int(m.group(2)))
            if m.group(4) == '+':
                dtend = dtstart + 3600*int(m.group(5)) + 60*int(m.group(6))   
            elif m.group(4) == '-':
                dtend = self.mktime(int(m.group(5)), int(m.group(6)))
                if dtend < dtstart: dtend += 86400
            else:
                dtend = dtstart
            summary = m.group(7)
            m = re_textref.match(summary)
            if m:
                summary, textref = m.groups()
            else:
                textref = None
            return self.events.append((dtstart, dtend, summary, self.location, textref))

        print "invalid line: `%s'" % line

    def parse_file(self, f):
        for line in f: self.parse_line(line)

    def write_files(self, base):
        format = "%s_%%0%dd.ics" % (base, len(str(len(self.events))))
        i = 0
        for e in self.events:
            i += 1
            f = open(format % i, "wb")
            self.export_event(f, *e)
            f.close()
        return format % i

    @staticmethod
    def split_group(e):
        try:
            group, summary = map(str.strip, e[2].split(':', 1))
            return (group, summary)
        except ValueError:
            return (None, e[2])

    def write_grouped_files(self, base):
        for group in set([group for group, summary in map(Events.split_group, self.events)]):
            filename = "%s_%s.ics" % (base, group.replace(' ', '_').lower())
            print "generating", filename, "..."
            f = open(filename, "wb")
            for e in self.events:
                eg, ed = Events.split_group(e)
                if eg != group:
                    continue
                e = list(e)
                e[2] = ed
                self.export_event(f, *e)
            f.close()

    def write_single_file(self, base):
        f = open(base + ".ics", "wb")
        for e in self.events:
            self.export_event(f, *e)
            f.write("\r\n")
        f.close()

    def writetext(self, f, s):
        f.write(s + "\r\n")

    def export_event(self, f, dtstart, dtend, summary, location, textref):
        f.write("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n")
        f.write("PRODID:-//%s//NONSGML v1.0//EN\r\n" % PRODID)
        f.write("BEGIN:VEVENT\r\n")
        f.write(time.strftime("DTSTART:%Y%m%dT%H%M%SZ\r\n", time.gmtime(dtstart)))
        f.write(time.strftime("DTEND:%Y%m%dT%H%M%SZ\r\n", time.gmtime(dtend)))
        if summary: f.write("SUMMARY:%s\r\n" % summary.replace(': ', ':').replace(':', ' - '))
        if location: f.write("LOCATION:%s\r\n" % location)
        if textref:
            try:
                self.writetext(f, "DESCRIPTION:" + self.texts[textref].replace("\n", "\\n"))
            except KeyError:
                print "reference to undefined text `%s'" % textref
        if self.alarm:
            f.write("BEGIN:VALARM\r\n")
            f.write(time.strftime("TRIGGER;VALUE=DATE-TIME:%Y%m%dT%H%M%SZ\r\n", time.gmtime(dtstart - self.alarm * 60)))
            f.write("ACTION:AUDIO\r\nEND:VALARM\r\n")
        f.write("END:VEVENT\r\nEND:VCALENDAR\r\n")



def opterr(msg):
    print >>sys.stderr, "Error:", msg
    print "Usage:", sys.argv[0], "[-single|-grouped] [-alarm <minutes>] [-adjust +/-<hours>] <input file>"
    sys.exit(2)

if __name__ == "__main__":
    infile = None
    single = False
    grouped = False
    alarm = None
    tzadj = 0

    args = sys.argv[1:]
    while args:
        arg = args.pop(0)
        if not arg.startswith('-'):
            if infile:
                opterr("multiple input files specified")
            infile = arg
            continue
        arg = arg.lstrip('-').lower()
        if arg == 'single':
            single = not(single)
        elif arg == 'grouped':
            grouped = not(grouped)
        elif arg == 'alarm':
            try:
                alarm = int(args.pop(0))
            except IndexError:
                opterr("missing argument for -alarm")
            except ValueError:
                opterr("expected numerical argument for -alarm")
        elif arg == 'adjust':
            try:
                tzadj = int(args.pop(0)) * 3600
            except IndexError:
                opterr("missing argument for -adjust")
            except ValueError:
                opterr("expected numerical argument for -adjust")
    if not infile:
        opterr("no input file specified")

    outbase = os.path.splitext(infile)[0]

    e = Events()
    if tzadj:
        e.tzadj = tzadj
    if alarm:
        e.alarm = alarm

    try:
        e.parse_file(open(infile, "r"))
    except IOError:
        print >>sys.stdout, "Error: cannot read input file"
        sys.exit(1)

    if single:
        try:
            e.write_single_file(outbase)
        except IOError, e:
            print >>sys.stdout, "Error: cannot write output files -", e
            sys.exit(1)
    elif grouped:
        try:
            e.write_grouped_files(outbase)
        except IOError, e:
            print >>sys.stdout, "Error: cannot write output files -", e
            sys.exit(1)
    else:
        try:
            lastfile = e.write_files(outbase)
        except IOError, e:
            print >>sys.stdout, "Error: cannot write output files -", e
            sys.exit(1)
        print "created %d files, last was `%s'" % (len(e.events), lastfile)
