#!/usr/bin/python

import os
import os.path
import sys
import re
import datetime
import time
from optparse import OptionParser
import locale

DEFAULT_CALENDAR = "~/systeme/agenda/agenda.ics"
# If not given "-e", assume the end is in DEFAULT_END days
DEFAULT_END = 30

class ICalReader:

    def __init__(self, target):
        self.events = []
        self.iCalFiles = []
        if not os.path.exists(target):
            raise IOError("This is neither a file nor a directory !")
        if os.path.isdir(target):
            for file in os.listdir(target):
                if os.path.isfile(os.path.join(target, file)) and file.endswith(".ics"):
                    self.iCalFiles.append(os.path.join(target, file))
        if os.path.isfile(target):
            self.iCalFiles = [target]
        self.readEvents()

    def calendars(self):
        return self.iCalFiles

    def readEvents(self):
        self.events = []
        startRe = re.compile("^BEGIN:VEVENT")
        endRe = re.compile("^END:VEVENT")
        for file in self.iCalFiles:
            lines = open(file).readlines()
            inEvent = False
            eventLines = []
            for line in lines:
                if startRe.match(line):
                    inEvent = True
                    eventLines = []

                if inEvent:
                    eventLines.append(line)
                    if endRe.match(line):
                        self.events.append(self.parseEvent(eventLines))
                        inEvent = False

        return self.events

    def parseEvent(self, lines):
        event = ICalEvent()
        startDate = None
        rule = None
        endDate = None
        cat = None
        exceptionDates = []
        summaryRe = re.compile("^SUMMARY:(.*)")
        startDateRe = re.compile("^DTSTART(;VALUE=DATE)?:(.*)")
        endDateRe = re.compile("^DTEND(;VALUE=DATE)?:(.*)")
        exDateRe = re.compile("^EXDATE;VALUE=DATE:(.*)")
        ruleRe = re.compile("^RRULE:(.*)")
        catRe = re.compile("^CATEGORIES:(.*)")
        event.lines = lines
        
        for line in lines:
            if summaryRe.match(line):
                event.summary = summaryRe.match(line).group(1)
            elif startDateRe.match(line):
                startDate = parseDate(startDateRe.match(line).group(2))
            elif endDateRe.match(line):
                endDate = parseDate(endDateRe.match(line).group(2))
            elif exDateRe.match(line):
                exceptionDates.append(parseDate(exDateRe.match(line).group(1)))
            elif ruleRe.match(line):
                rule = ruleRe.match(line).group(1)
            elif catRe.match(line):
                cat = catRe.match(line).group(1).split(",")

        event.startDate = startDate
        event.endDate = endDate
        if rule:
            event.addRecurrenceRule(rule, exceptionDates)
        if cat:
            event.categories = cat
        return event

    def selectEvents(self, selectFunction):
        self.events.sort()
        events = filter(selectFunction, self.events)
        return events

    def todaysEvents(self, event):
        return event.startsToday()

    def tomorrowsEvents(self, event):
        return event.startsTomorrow()

    def eventsInCategories(self, cats):
        self.events.sort()
        ret = []
        for event in self.events:
            if event.isInCategories(cats):
                ret.append(event)
        return ret

    def eventsFor(self, date):
        self.events.sort()
        ret = []
        for event in self.events:
            if event.happensOn(date):
                ret.append(event)
        return ret

    def eventsBetween(self, start, end):
        self.events.sort()
        ret = []
        for event in self.events:
            if event.isBetween(start, end):
                ret.append(event)
        return ret
        


class ICalEvent:
    def __init__(self):
        self.dateSet = None
        self.lines = []
        self.categories = []
        self.startDate = datetime.datetime.today()
        self.endDate = datetime.datetime.today()

    def __str__(self):
        cats = ""
        if self.categories:
            cats = " (%s)" % ", ".join(self.categories)
        return "%s: %s%s" % (self.startDate, self.summary, cats)

    def __eq__(self, otherEvent):
        return self.startDate == otherEvent.startDate \
           and self.endDate == otherEvent.endDate

    def __eq__(self, otherEvent):
        return not self == otherEvent

    def __len__(self):
        return self.endDate - self.startDate

    def __lt__(self, otherEvent):
        """Sort using the startDate. And events lasting the whole day are first"""
        if self.startDate == otherEvent.startDate:
            if self.lastsWholeDay() and not otherEvent.lastsWholeDay():
                return True
            elif otherEvent.lastsWholeDay() and not self.lastsWholeDay():
                return False
            else: # same type, same start : considered equal for __lt__
                return False
        else:
            if self.startDate.__class__ != otherEvent.startDate.__class__:
                self.startDate = datetime.datetime.combine(self.startDate, datetime.time(0))
                otherEvent.startDate = datetime.datetime.combine(otherEvent.startDate, datetime.time(0))
            return self.startDate < otherEvent.startDate
    
    def __le__(self, otherEvent):
        """Use __lt__. Equality happens when the startDate are equal and of the same type
           (both last the whole day or both do not last the whole day."""
        return self < otherEvent or \
        ( self.startDate == otherEvent.startDate and type(self.startDate) == type(otherEvent.startDate) )

    def __gt__(self, otherEvent):
        """ ">" is the opposite of "<=". """
        return not self <= otherEvent
    
    def __ge__(self, otherEvent):
        """ ">=" is the opposite of "<". """
        return not self < otherEvent

    def addRecurrenceRule(self, rule, exceptionDates=[]):
        self.dateSet = DateSet(self.startDate, self.endDate, rule, exceptionDates)

    def startsToday(self):
        return self.happensOn(datetime.date.today())

    def startsTomorrow(self):
        tomorrow = datetime.date.today() + datetime.timedelta(days=1)
        return self.happensOn(tomorrow)

    def happensOn(self, date):
        if self.dateSet:
            return self.dateSet.includes(date)
        else:
            if type(self.startDate) == datetime.date:
                return self.startDate <= date and self.endDate > date
            else:
                return self.startDate.date() == date

    def startTime(self):
        return self.startDate

    def isInCategories(self, cats):
        for cat in cats:
            if cat in self.categories:
                return True
        return False

    def isNotInCategories(self, cats):
        return not self.isInCategories(cats)

    def isBetween(self, start, end):
        return self.isAfter(start) and self.isBefore(end)

    def happensBetween(self, start, end):
        date = start
        while date < end:
            if self.happensOn(date):
                return True
            date = date + datetime.timedelta(days=1)
        return False
        
    def isAfter(self, date):
        return self.startDate >= date

    def isBefore(self, date):
        if not self.dateSet:
            return self.endDate < end
        elif self.dateSet.untilDate:
            return self.dateSet.untilDate < end
        else:
            return False

    def happensBefore(self, date):
        return self.startDate < date
    
    def lastsWholeDay(self):
        """If the event lasts the whole day, startDate is of type datetime.date.
           Else, startDate is of type datetime.datetime."""
        return type(self.startDate) == datetime.date


class ICalWriter:

    def __init__(self):
        self.events = []

    def addEvent(self, event):
        self.events.append(event)

    def write(self, target):
        if not self.events:
            return False
        file = open(target, 'w')
        file.write("BEGIN:VCALENDAR\nVERSION:2.0\n\n")
        for event in self.events:
            file.writelines(event.lines)
            file.write('\n')
        file.write("\nEND:VCALENDAR\n")
        file.close()


class DateSet:
    def __init__(self, startDate, endDate, rule, exceptionDates=[]):
        self.startDate = startDate
        self.endDate = endDate
        self.frequency = None
        self.count = None
        self.untilDate = None
        self.interval = 1
        self.byMonth = None
        self.byDay = None
        self.exceptionDates = exceptionDates
        self.parseRecurrenceRule(rule)
    
    def parseRecurrenceRule(self, rule):
        freqRe = re.compile("FREQ=(.*?);")
        countRe = re.compile("COUNT=(\d*)")
        untilRe = re.compile("UNTIL=(.*?);")
        intervalRe = re.compile("INTERVAL=(\d*)")
        byMonthRe = re.compile("BYMONTH=(.*?);")
        byDayRe = re.compile("BYDAY=(.*?);")
        if freqRe.search(rule) :
            self.frequency = freqRe.search(rule).group(1)
        if countRe.search(rule) :
            self.count = int(countRe.search(rule).group(1))
        if untilRe.search(rule) :
            self.untilDate = parseDate(untilRe.search(rule).group(1))
        if intervalRe.search(rule) :
            self.interval = int(intervalRe.search(rule).group(1))
        if byMonthRe.search(rule) :
            self.byMonth = byMonthRe.search(rule).group(1)
        if byDayRe.search(rule) :
            self.byDay = byDayRe.search(rule).group(1)
        
    def includes(self, date):
        if date in self.exceptionDates:
            return False
        if date == self.startDate:
            return True
        if self.untilDate and date > self.untilDate:
            return False
        if self.frequency == 'DAILY':
            return self.includesRecurrent(date, lambda d: d+datetime.timedelta(days=1*self.interval))
        elif self.frequency == 'WEEKLY':
            return self.includesRecurrent(date, lambda d: d+datetime.timedelta(days=7*self.interval))
        elif self.frequency == 'MONTHLY':
            return self.includesRecurrent(date, self.addMonth)
        elif self.frequency == 'YEARLY':
            return self.includesRecurrent(date, lambda d: d.replace(year=d.year+self.interval))
        return False

    def addMonth(self, date):
        if date.month + self.interval > 12 :
            return date.replace(year=date.year+1, month=date.month+self.interval-12)
        return date.replace(month=date.month+self.interval)

    def includesRecurrent(self, date, addFunc):
        """increment must be of type datetime.timedelta"""
        d = self.startDate
        counter = 0
        while(d < date):
            if self.count:
                counter += 1
                if counter >= self.count:
                    return False
            d = addFunc(d)
            if d == date:
                return True
        return False


# -------------------------------------


def viewEvents(events, start, end):
    locale.setlocale(locale.LC_ALL, '')
    events.sort()
    date = start
    while date < end:
        viewDay(date, events)
        date = date + datetime.timedelta(days=1)

def viewDay(date, events):
    eventsThisDay = []
    today = datetime.date.today()
    tomorrow = today + datetime.timedelta(days=1)
    for event in events:
        if event.happensOn(date): # What follows is only formatting
            if type(event.startDate) == datetime.date: # It's during the whole day
                eventText = event.summary
            else: # It has start and end times
                eventStartTz = event.startDate - datetime.timedelta(seconds=time.timezone)
                eventTime = eventStartTz.strftime("%X")[:-3] # drop seconds
                if event.endDate != event.startDate:
                    eventEndTz = event.endDate - datetime.timedelta(seconds=time.timezone)
                    eventTime = "%s-%s" % (eventTime, eventEndTz.strftime("%X")[:-3])
                eventText = "%s: %s" % (eventTime, event.summary)
            cats = ""
            if event.categories:
                cats = " (%s)" % ", ".join(event.categories)
            eventText = eventText + cats
            eventsThisDay.append(eventText) # Remove date (1st field)
    if eventsThisDay:
        if date == today:
            print "--- Today (%s) ---" % date.strftime("%d/%m")
        elif date == tomorrow:
            print "--- Tomorrow (%s) ---" % date.strftime("%d/%m")
        else:
            print "--- %s ---" % date.strftime("%a %x")
        for event in eventsThisDay:
            print event
        print

def parseDate(dateStr):
    if len(dateStr) == 4:
        year = datetime.date.today().year
        month = int(dateStr[0:2])
        day = int(dateStr[2:4])
        return datetime.date(year, month, day)
    elif len(dateStr) >= 8:
        year = int(dateStr[0:4])
        if year < 1970:
            year = 1970
        month = int(dateStr[4:6])
        day = int(dateStr[6:8])
        try:
            hour = int(dateStr[9:9+2])
            minute = int(dateStr[11:11+2])
            return datetime.datetime(year, month, day, hour, minute)
        except:
            return datetime.date(year, month, day)
    return False


def main():
    usage = "usage: %prog [-r file] [-n days] [-d date | -s start -e end] [-c cat1,cat2,...] [-x cat1,cat2,...] [-w file]"
    version = "Version: "+"$Rev: 97 $"[6:-2] \
             +", Date: "+"$Date: 2005-05-30 20:07:06 +0200 (lun, 30 mai 2005) $"[7:-2]
    parser = OptionParser(usage=usage, version=version)
    parser.add_option("-r", "--read", dest="filefrom", help="read calendar from FILE. "\
                     +"If FILE is a directory, read all iCalendar files in it.", metavar="FILE")
    parser.add_option("-n", "--next", dest="next", help="view what's on the menu for the next NEXTDAYS days", \
                      metavar="NEXTDAYS")
    parser.add_option("-d", "--date", dest="date", help="view what's on this DATE. " \
                     + "Example: 20041115 or 1115")
    parser.add_option("-s", "--start-date", dest="startDate", help="view from this DATE. " \
                     + "Example: 20041115 or 1115", metavar="DATE")
    parser.add_option("-e", "--end-date", dest="endDate", help="view until this DATE. " \
                     + "Example: 20041115 or 1115", metavar="DATE")
    parser.add_option("-c", "--categories", dest="categories", \
                      help="select these categories (comma-separated)", metavar="CAT1,CAT2,...")
    parser.add_option("-x", "--exclude-categories", dest="excludecategories", \
                      help="unselect these categories (comma-separated)", metavar="CAT1,CAT2,...")
    parser.add_option("-w", "--write", dest="fileto", help="write calendar to FILE", metavar="FILE")
    options, args = parser.parse_args()
    if len(args) > 0:
        parser.error("illegal arguments: %s"% ", ".join(args))

    if options.filefrom:
        reader = ICalReader(os.path.expanduser(options.filefrom))
    else:
        reader = ICalReader(os.path.expanduser(DEFAULT_CALENDAR))
        #parser.error("You must give me a file to read")
    start = end = None
    if options.categories: 
        tmpEventsList = reader.selectEvents(lambda e: e.isInCategories(options.categories.split(",")))
        reader.events = tmpEventsList
    if options.excludecategories: 
        tmpEventsList = reader.selectEvents(lambda e: e.isNotInCategories(options.excludecategories.split(",")))
        reader.events = tmpEventsList
    if options.date:
        start = parseDate(options.date)
        end = start + datetime.timedelta(days=1)
    if options.startDate:
        start = parseDate(options.startDate)
    if options.endDate:
        end = parseDate(options.endDate)
        # If we want to include the end date :
        end = end + datetime.timedelta(days=1)
    if options.next:
        if not start:
            start = datetime.date.today()
        end = start + datetime.timedelta(days=int(options.next))
    if end and not start:
        tmpEventsList = reader.selectEvents(lambda e: e.happensBefore(end))
        reader.events = tmpEventsList
    if start and not end:
        tmpEventsList = reader.selectEvents(lambda e: e.isAfter(start))
        reader.events = tmpEventsList
    if start and end:
        tmpEventsList = reader.selectEvents(lambda e: e.happensBetween(start, end))
        reader.events = tmpEventsList
    if options.fileto:
        writer = ICalWriter()
        for event in reader.events:
            writer.addEvent(event)
        writer.write(os.path.expanduser(options.fileto))
    else:
        if not start:
            start = datetime.date.today()
        if not end:
            end = datetime.date.today() + datetime.timedelta(days=DEFAULT_END)
        viewEvents(reader.events, start, end)

    
if __name__ == '__main__':
    main() 

