# Copyright 1999-2004, Fred Steinberg, Brown Bear Software

# Event

# An Event has a string to display, and a link for either a URL or
# Javascript popup. It can also, optionally, have a start time, end time,
# and/or RepeatInfo object for repeating. And then there are other things, too.

# Methods include:
#  new
#  getTimeString
#  isRepeating
#  getHTML
#  applies
#  equals
#  excludeThisInstance
#  addToDateHash
#  getIncludedOverrides ($incInfo) - check for included color, border overrides
#  getCategoryOverrides ($prefs,)  - check for included color, border overrides
#  private - return true if event is not to be included in other cals
#  privatePopup - return true if popup text is not to be included in other cals

# The Get/Set methods are handled by AUTOLOAD.

package Event;
use strict;
use Calendar::EventSorter;
use vars qw ($AUTOLOAD %validField);

use overload ('=='       => 'equals',
              'fallback' => 'true');

# If mod_perling, This had better be a constant hash.
# Date and TZoffset and Prefs are not stored!
BEGIN {
    foreach (qw(text link popup export repeatInfo startTime endTime id owner
                drawBorder bgColor fgColor includedFrom category isTentative
                mailTo mailCC mailBCC mailText timePeriod
                Date TZoffset Prefs
                reminderTimes reminderTo subscriptions)) {
        $validField{$_}++;
    }
}

# Pass hash pairs
sub new {
    my $class = shift;
    my %args = ('text'       => '',
                'link'       => '',
                'popup'      => '',
                'export'     => 'Public',
#                repeatInfo => ,    Notice that
#                startTime  => ,            these keys (and others)
#                endTime    => ,                 are optional
                @_);
    my $self = {};
    bless $self, $class;

    my ($key, $value);
    while (($key, $value) = (each %args)) {
        $self->{$key} = $value if $value;
    }

    # some are special; false is ok, undef is not. silly.
    foreach (qw/startTime endTime/) {
        $self->{$_} = $args{$_} if (defined ($args{$_}) and $args{$_} ne '');
    }

    # If a Time Period defined, don't store start/end times
    if ($args{timePeriod}) {
        delete $self->{startTime};
        delete $self->{endTime};
    }

    # if categories a ref to a list, make it a scalar
    if (ref $self->{category}) {
        $self->{category} = join "\035", @{$self->{category}};
    }

    $self;
}

sub AUTOLOAD {
    my $self = shift;
    my $name = $AUTOLOAD;
    $name =~ s/.*://;           # get rid of package names, etc.
    return unless $name =~ /[^A-Z]/;  # ignore all cap methods; e.g. DESTROY 

    # Make sure it's a valid field, eh wot?
    die "Bad Field Name to Event! '$name'\n" unless $validField{$name};

    $self->{$name} = shift if (@_);
    $self->{$name};
}

# Shallow copy; has references to same thingys (e.g. repeatInfo)
sub copy {
    my $self = shift;
    my $copy = Event->new (%$self);
    if ($copy->timePeriod) {
        # need to copy separately, new() doesn't get them, and they might
        # have been adjusted for TZ offset. ack.
        $copy->startTime ($self->startTime);
        $copy->endTime   ($self->endTime);
    }
    return $copy;
#    return Event->new (%$self);
}

# Return passed in text, possibly with HTML possibly escaped and things
# that look something like URLs converted to href links. Newlines are
# converted to <br>, unless simplistic check for HTML tags succeeds.
sub _escapeThis {
    my ($text, $escaped, $doHREFs, $linkColor) = @_;
    return '' unless $text;
    my $noBR = ($text =~ /<[^>]*>/); # not very good, but good enough
    if ($escaped) {
        $text =~ s/</&lt;/g;
        $text =~ s/>/&gt;/g;
    }
    # If there's not href or img in there already, put in href and mailto: tags
    if ($doHREFs and $text !~ /<\s*(a\s+)|(img)/i) {
        # inherit colors
        # my $style = qq (style="color: inherit;");
        # except 'inherit' doesn't work on IE, so we have to do this
        my $style = $linkColor ? qq (style="color: $linkColor") : '';

        $text =~ s {([-\w.]+@[-\w]+\.[-\w.]+)}
                   {<a href="mailto:$1" $style>$1</a>}g;

        my $target = '';
        $target = ' target="_blank"' if ($doHREFs =~ /newwindow/i);
        local $^W = undef;
        # look for 'http' first, then for 'www' if no 'http' found
        $text =~ s {http(s)?://(\S+)}
                   {<a href="http$1://$2"$target $style>$2</a>}g
          or $text =~ s {(www\.[\w-]+\.[\w-]+)}
                        {<a href="http://$1"$target $style>$1</a>}g
    }

    $text =~ s/\n/<br>/g unless $noBR;
    $text;
}

# args are flags for ($escapeIt, $doHREFs, $fgColor)
sub escapedText {
    my $self = shift;
    _escapeThis ($self->text, @_);
}
sub escapedPopup {
    my $self = shift;
    _escapeThis ($self->popup, @_);
}

# See if event text matches passed in regex (use quotemeta for exact match).
# 2nd param is where to look; one of ['text', 'popup', 'both']
sub matchesText {
    my ($self, $regex, $where) = @_;
    return undef unless defined $regex;
    $where ||= 'both';
    if ($where =~ /text|both/i) {
        return 1 if (defined $self->text and $self->text =~ /$regex/);
    }
    if ($where =~ /popup|both/i) {
        return 1 if (defined $self->popup and $self->popup =~ /$regex/);
        return 1 if (defined $self->link  and $self->link  =~ /$regex/);
    }
    return undef;
}

# See if event is in any of the categories (typically for filtering)
# Pass one category, or ref to list of them.
sub inCategory {
    my ($self, $cats) = @_;
    $cats = [$cats] unless ref $cats;
    my %myCats = map {$_ => 1} $self->getCategoryList;
    return undef unless (keys %myCats);
    foreach my $cat (@$cats) {
        return 1 if ($myCats{$cat});
    }
    return undef;
}

# Return only first category in category list; undef if no primary
sub primaryCategory {
    my $self = shift;
    my ($primaryCat, @moreCats) = $self->getCategoryList;
    return $primaryCat;
}

# Return categories as list. Make sure Primary category always first.
sub getCategoryList {
    my $self = shift;
    my $cats = $self->{category} || return ();
    my @cats = split "\035", $cats;
    return @cats;
}
sub setCategoryList {
    my ($self, @cats) = @_;
    $self->{category} = join "\035", @cats;
    return @cats;
}
# Return comma separated string of categories, or undef if none
sub getCategoryScalar {
    my $self = shift;
    my $cats = $self->{category} || return undef;
    $cats =~ s/\035/,/g;
    return $cats;
}

sub hasTime {
    my $self = shift;
    return 1 if ($self->timePeriod or $self->startTime);
}
# Set or return startTime, which might be defined in a time period
sub startTime {
    my $self = shift;
    $self->{startTime} = shift if (@_);
    return $self->{startTime} if exists ($self->{startTime});
    return undef unless ($self->timePeriod and $self->Prefs);
    $self->_getTimesFromPeriod;
    return $self->{startTime};
}
sub endTime {
    my $self = shift;
    $self->{endTime} = shift if (@_);
    return $self->{endTime} if exists ($self->{endTime});
    return undef unless ($self->timePeriod and $self->Prefs);
    $self->_getTimesFromPeriod;
    return $self->{endTime};
}
sub _getTimesFromPeriod {
    my $self = shift;
    my ($name, $start, $end, $display) =
                          $self->Prefs->getTimePeriod ($self->timePeriod);
    ($self->{startTime}, $self->{endTime}) = ($start, $end);
}

# If object method, pass 'start', 'end', or 'both', and prefs object
#  - If event has no times, returns undef
#  - If event has Time Period, get times for that
#  - If event has start but no end and you ask for both, returns just start
# If class method, just pass a time (as int) and militaryTimeP
sub getTimeString {
    my $caller = shift;
    my (@times, $milTimeP);
    if (ref ($caller)) {
        my ($which, $prefs) = @_;
        $milTimeP = $prefs->MilitaryTime;
        my ($start, $end);
        $start = $caller->startTime;
        $end   = $caller->endTime;
        push @times, $start if ($which =~ /start|both/ and defined $start);
        push @times, $end   if ($which =~ /end|both/   and defined $end);
    } else {
        push @times, shift;
        $milTimeP = shift;
    }
    my ($theString, $secondTime);
    foreach my $time (@times) {
        next if (!defined $time or $time eq '');   # shouldn't happen, but...
        $theString .= ' - ' if ($secondTime++);
        my ($hour, $minute) = (int ($time / 60), $time % 60);
        if ($milTimeP) {
            $theString .= sprintf '%d:%.2d', $hour, $minute;
        } else {
            my $string;
            if ($hour <= 12) {
                # midnight = 0;
                $string = sprintf '%d:%.2d', ($hour ? $hour : 12), $minute;
            } else {
                $string = sprintf '%d:%.2d', $hour - 12, $minute;
            }
            $string .= ($hour > 11) ? 'pm' : 'am';
            $theString .= $string;
        }
    }
    $theString;
}

# Return what date we fall on, adjusted for timezone
# Pass date, offset in hours (as stored in prefs)
sub getDisplayDate {
    my ($self, $date, $offset) = @_;
    my $start = $self->startTime;
    return $date unless (defined $start and $offset);
    $start += $offset * 60;

    if ($start < 0) {
        my $numDays = int ($start/-1440) + 1;     # 1440 = 24* 60
        return $date - $numDays;
    } elsif ($start >= 1440) {
        my $numDays = int ($start/1440);
        return $date + $numDays;
    }
    return $date;

#     return $date - 1 if $start < 0;
#     return $date + 1 if $start >= 24*60;
#     return $date;
}
# Return (startTime, endTime) adjusted by offset.
sub getDisplayTime {
    my ($self, $offset) = @_;
    my ($start, $end) = ($self->startTime, $self->endTime);
    return ($start, $end) unless $offset;
    foreach ($start, $end) {
        next unless defined;
        $_ += $offset * 60;
        $_ %= 1440;
#         if ($_ < 0) {
#             $_ += 24*60;
#         } elsif ($_ >= 24*60) {
#             $_ -= 24*60;
#         }
    }
    return ($start, $end);
}
# Change times, store date
# Return -1 if date decremented, 1 if incremented, 0 if unchanged
sub adjustForTimezone {
    my ($self, $date, $offsetHours) = @_;
    my ($start, $end) = ($self->startTime, $self->endTime);
    my $offset = $offsetHours * 60;
    my $ret = 0;
    return $self->TZoffset if (defined $self->TZoffset);

    if (defined $start) {
        $start += $offset;
        if ($start < 0) {
            $date -= int ($start/-1440) + 1;
            $start %= 1440;
            $ret = -1;
        } elsif ($start >= 24*60) {
            $date += int ($start/1440);
            $start %= 1440;
            $ret = 1;
        }

        $self->startTime ($start);

        if (defined $end) {
            $end += $offset;
            $self->endTime ($end % 1440);
        }
    }

    $self->Date ($date);       # not stored on disk
    $self->TZoffset ($ret);
    return $ret;
}


sub isRepeating {
    my $self = shift;
    return defined $self->{'repeatInfo'};
}

# Return ('', $linkText) if it's a link, ($popupText, '') if not.
sub textToPopupOrLink {
    my ($ref, $text) = @_;
    return ('', '') unless $text;
    if ($text =~ /^((https?|mailto|ftp|file):)|^www\.[^ .]+\.[^ .]/s) {
        return ('', ($1 ? $text : "http://$text"));
    } else {
        return ($text, '');
    }
}

# The popup code is an HTTP link to display the popup
sub getHTML {
    my ($self, $args) = @_;

    die "Unexpected args to Event::getHTML\n" unless (ref $args eq 'HASH');

    my ($op, $calName, $date, $prefs, $i18n, $textFG,
        $eventFace, $eventSize, $timeFace, $timeSize, $textID, $hideTimes);

    $op        = $args->{op};
    $calName   = $args->{calName};
    $date      = $args->{date};
    $prefs     = $args->{prefs};
    $i18n      = $args->{i18n};
    $textFG    = $args->{textFG};
    $eventFace = $args->{eventFace};
    $eventSize = $args->{eventSize};
    $timeFace  = $args->{timeFace};
    $timeSize  = $args->{timeSize};
    $textID    = $args->{textID};
    $hideTimes = $args->{hideTimes};

    my $htmlText;

    # if displaying tentative event, add a tentative tag
    if ($self->isTentative) {
        $htmlText .= "<span class='EventTag Tentative'>" .
                           $i18n->get ('Pending Approval') . "</span><br>";
    }

    # includedFrom could be wrong if db is cached
    if ($self->includedFrom and $self->includedFrom eq $calName) {
        $self->includedFrom (undef);
    }

    if ($self->includedFrom and $textID) {
        $htmlText .= qq (<span class="IncludeTag">$textID</span><br>);
    }

    # workaround for workaround in BlockView (planner view...)
    my $source = $self->includedFrom || '';
    $source =~ s/\s*$//;

    my @catList = $self->getCategoryList;
    if (@catList) {
        my @names;
        foreach my $catName (@catList) {
            my $cat = $prefs->category ($catName) ||
                      MasterDB->new->getPreferences->category ($catName);
            push @names, $catName
                if ($cat and $cat->showName);
        }
        if (@names) {
            my $tags = join '<br>', @names;
            $htmlText .= qq (<span class="EventTag Category">$tags</span><br>);
        }
    }

    # if wanted, display extra tags
    if (my $extraTags = $prefs->EventTags) {
        $htmlText .= '<span class="EventTag Owner">' . $self->owner .
                     '</span><br>'
            if ($self->owner and $extraTags =~ /owner/);
        if ($extraTags =~ /export/) {
            my $tag = $self->displayString ($i18n) || $i18n->get ('Public');
            $htmlText .= qq (<span class="EventTag Export">$tag</span><br>);
        }
    }

    my @timeStrings;
    unless ($hideTimes) {
        if (my $period = $self->timePeriod) {
            my $p = $prefs;
            my $orig;
            if ($self->includedFrom) { # gads...this is rather ugly
                $orig = $prefs;
                $p = Preferences->new ($source);
                $self->Prefs ($p);
                delete $self->{startTime};
                delete $self->{endTime};
            }
            my ($name, $start, $end, $display) = $p->getTimePeriod ($period);

            $display ||= '';   # in case period doesn't exist
            if ($display eq 'period' or $display eq 'both') {
                push @timeStrings, $name;
            }
            if ($display eq 'times' or $display eq 'both') {
                push @timeStrings, $self->getTimeString ('both', $prefs);
            }
            $self->Prefs ($orig) if $orig;
        } elsif (defined $self->startTime) {
            push @timeStrings, $self->getTimeString ('both', $prefs);
        }
    }

    my @sizes = ('xx-small', 'x-small', 'small', 'medium', 'large', 'x-large',
                 'xx-large', 'xx-large');
    foreach ($timeSize, $eventSize) {
        next unless defined;
        $_ = $sizes[$_]
                if ($_ ne 'smaller' and $_ ne 'larger');
    }
    if (@timeStrings) {
        my @styles;
        push @styles, "font-family: $timeFace" if $timeFace;
        push @styles, "font-size: $timeSize"   if defined $timeSize;
        my $style = '';
        if (@styles) {
            $style = 'style="' . join (';', @styles) . '"';
        }
        $htmlText .= qq (<span class="TimeLabel" $style>);
        $htmlText .= join ': ', @timeStrings;
        $htmlText .= '</span><br>';
    }

    my $eventStyles = '';
    if ($eventFace or $eventSize or $textFG) {
        my @styles;
        push @styles, "font-family: $eventFace" if $eventFace;
        push @styles, "font-size: $eventSize"   if defined $eventSize;
        push @styles, "color: $textFG"          if defined $textFG;
        $eventStyles = join ';', @styles;
    }
    my $style = '';
    if ($eventStyles) {
        $style = qq (style="$eventStyles");
    }

    # Display event text, or maybe special string (e.g. 'Out of Office')
    if ($self->includedFrom and !$self->public and !$self->privatePopup) {
        $htmlText .= qq (<span class="PrivacyLabel" $style>) .
                     ($self->displayString ($i18n) || '') . '</span>';
    } else {
        # escape HTML if we need to, converting \n to <br>, and maybe
        # detect email address and http links
        my $escapeIt = $prefs->EventHTML =~ /none/;
        my $eventText;

        if ($self->{'link'} and
            (!$self->includedFrom or $self->public)) {
            $eventText = $self->escapedText ($escapeIt);
            $style = qq (style="$eventStyles");
            $htmlText .= qq (<a href="$self->{link}" $style>) .
                         qq ($eventText</a>);
        } elsif ($self->{'popup'} and
                 (!$self->includedFrom or $self->public)) {
            $eventText = $self->escapedText ($escapeIt);
            my $id = $self->id;

            my $width = $prefs->PopupWidth   || 250;
            my $height = $prefs->PopupHeight || 350;

            my $jsParams = "'$calName', '$date', '$id', '$source', " .
                           "'$width', '$height'";
            $style = qq (style="$eventStyles");
            my $jsStuff = qq{<a href="JavaScript:PopupWindow ($jsParams)" } .
                          qq{$style>} .
                          qq {$eventText</a>};
            $jsStuff =~ s/(["'\\])/\\$1/g;     # '"])
            $jsStuff =~ s/\n//g;
            $htmlText .= qq (<div class="EventLink">);
            $htmlText .= qq {<script>document.write ("$jsStuff");</script>};
            my $url = '';
            if ($op) {
                $url = $op->makeURL ({Op     => 'PopupWindow',
                                      ID     => $id,
                                      Date   => $date,
                                      Source => $source,
                                      DoneURL =>
                                          $op->makeURL ({Op => $op->opName})});
            }
            $htmlText .= qq {<noscript><a href="$url">$eventText</a>} .
                            '</noscript>';
            $htmlText .= '</div>';
        } else {
            $eventText = $self->escapedText ($escapeIt, 'doHREFs', $textFG);
            $htmlText .= qq (<div $style>$eventText</div>);
        }
    }

    if (my $primaryCat = $self->primaryCategory) {
        $primaryCat =~ s/\W//g;
        $htmlText = qq (<div class="$primaryCat">$htmlText</div>);
    }

    my $ondbl = '';
    if ($op->userPermitted ('Edit')) {
        if ($source and $source =~ /^ADDIN (.*)/) {
            my $addIn = $1;
            $ondbl = "editEvent (-1, '$addIn')";
        } else {
            $ondbl = sprintf ("editEvent (%d, '%s')", $self->id,
                              ($source || $calName));
        }
        $ondbl = qq (ondblclick="$ondbl");
    }

    $style = defined $textFG ? qq (style="color: $textFG") : '';
    return qq (<div class="CalEvent" $ondbl $style>) .
          "$htmlText</div>";
}

# Return (fg, bg) colors based on event settings, inclusion, category
#   return included colors if included (and override set)
#   else, return event specific colors (if set)
#   else, return category colors (if set)
#   else, return colors from prefs
sub colors {
    my ($self, $calName, $prefs, $noDefault) = @_;

    my ($fgColor, $bgColor, $border, $textID);

    # included calendar colors specified?
    if ($self->includedFrom || '' ne $calName) {
        my ($fg, $bg, $bdr, $id) =
            $self->getIncludedOverrides ($prefs->Includes);
        $fgColor = $fg unless defined $fgColor;
        $bgColor = $bg unless defined $bgColor;
    }
    return ($fgColor, $bgColor) if (defined $fgColor and defined $bgColor);

    # event have it's own colors specified?
    $fgColor = $self->fgColor unless defined $fgColor;
    $bgColor = $self->bgColor unless defined $bgColor;
    return ($fgColor, $bgColor) if (defined $fgColor and defined $bgColor);

    # category colors specified?
    if (defined $self->primaryCategory) {
        my ($fg, $bg, $bdr) = $self->getCategoryOverrides ($prefs,
                                          MasterDB->new->getPreferences);
        $fgColor = $fg unless defined $fgColor;
        $bgColor = $bg unless defined $bgColor;
    }
    return ($fgColor, $bgColor) if (defined $fgColor and defined $bgColor);

    return ($fgColor, $bgColor) if $noDefault;

    $fgColor = $prefs->color ('EventFG') if (!defined $fgColor);
    $bgColor = $prefs->color ('EventBG') if (!defined $bgColor);

    return ($fgColor, $bgColor);
}

sub applies {
    my $self = shift;
    my ($date) = @_;

    # return true right away if it's not a repeating event
    return 1 unless $self->isRepeating();

    # otherwise, ask the RepeatInfo object
    return $self->{'repeatInfo'}->applies ($date);
}

sub equals {
    my ($e1, $e2, $backwards) = @_;
    return ($e1->{'id'} == $e2->{'id'});
}

# Use this to keep track of which instances of a repeating event we deleted
sub excludeThisInstance {
    my $self = shift;
    my ($date) = @_;
    # Simply pass it along to the RepeatInfo object, unless there isn't
    # one. 
    if ($self->isRepeating()) {
        $self->{'repeatInfo'}->excludeThisInstance ($date);
    }
}

# Set or Return ref to list of excluded date objs; return undef if not a
#   repeating event
sub exclusionList {
    my $self = shift;
    my $listRef = shift;
    # Simply pass it along to the RepeatInfo object, unless there isn't
    # one. 
    if ($self->isRepeating()) {
        return $self->{'repeatInfo'}->exclusionList ($listRef);
    }
    return;
}

# Pass through to EventSorter
sub sort {
    my ($class, $eventListRef, $sortPref) = @_;
    my $sorter = EventSorter->new (split (',', ($sortPref || ())));
    my $sortedListref = $sorter->sortEvents ($eventListRef);
    return @$sortedListref;
}

# Find all applicable dates for this event, add to the hash passed in.
# Notice that if there is no repeat info, we don't need to do anything.
sub addToDateHash {
    my $self = shift;
    my ($hash, $fromDate, $toDate, $prefs) = @_;

    return unless $self->isRepeating();

    $self->repeatInfo()->addToDateHash ($hash, $fromDate, $toDate,
                                        $self, $prefs);
}

sub getIncludedOverrides
{
    my $self = shift;
    my ($incInfo) = @_;
    my $incCal = $self->includedFrom();
    my ($fgColor, $bgColor, $border, $text);
    if ($incCal &&
        $incInfo->{$incCal}->{'Included'}) {
        $text = $incInfo->{$incCal}->{Text} || '';
        if ($incInfo->{$incCal}->{'Override'}) {
            $fgColor = $incInfo->{$incCal}->{'FG'};
            $bgColor = $incInfo->{$incCal}->{'BG'};
            $border  = $incInfo->{$incCal}->{'Border'} ? 1 : 0;
        }
    }
    ($fgColor, $bgColor, $border, $text);
}

sub getCategoryOverrides
{
    my $self = shift;
    my (@prefs) = @_;
    my $catName = $self->primaryCategory;
    return undef unless $catName;
    foreach my $prefs (@prefs) {
        my $cat = $prefs->category ($catName);
        if ($cat) {
            my $border = 1 if $cat->border;
            return ($cat->fg, $cat->bg, $border);
        }
    }
    undef;
}

sub public {
    my $self = shift;
    return (!defined $self->export or
            $self->export =~ /Public/i or
            $self->export eq '');
}

sub private {
    my $self = shift;
    return ($self->export and $self->export =~ /Private/i);
}

sub privatePopup {
    my $self = shift;
    return ($self->export and $self->export =~ /NoPopup/i);
}

sub displayString {
    my $self = shift;
    my $i18n = shift;
    return $i18n->get ('Private')       if ($self->private);
    return $i18n->get ('Private Popup') if ($self->privatePopup);
    return '' unless $self->export;
    return $i18n->get ('Unavailable')   if ($self->export =~ /Unavailable/i);
    return $i18n->get ('Out of Office') if ($self->export =~ /OutOfOffice/i);
    return '';
}

# -- Subscriptions
# 'subscriptions' looks like: "calname:a@b.com,c@d.com;otherCal:f@g.com"
# Need to specify which calendar we're interested in

sub isSubscribed {
    my ($self, $address, $calName) = @_;
    my $addrs = $self->getSubscribers ($calName); # comma joined list
    return ($addrs =~ /\b$address\b/i);
}

sub addSubscriber {
    my ($self, $address, $calName) = @_;
    my @calStrings = split /;/, ($self->subscriptions || '');
    my $foundIt;
    foreach (@calStrings) {
        next unless /^$calName:(.*)/;
        $foundIt = 1;
        $_ .= ',' if $1;
        $_ .= $address;
        last;
    }
    if (!$foundIt) {
        push @calStrings, "$calName:$address";
    }
    $self->subscriptions (join ';', @calStrings);
}

# Return comma joined list of addresses for specified calendar.
sub getSubscribers {
    my ($self, $calName) = @_;
    my @calStrings = split /;/, ($self->subscriptions || '');
    foreach (@calStrings) {
        next unless /^$calName:/;
        s/$calName://;
        return $_;
    }
    return '';
}



{
    # these are the fields that can get stored
    my %map = (a => 'text',
               b => 'link',
               c => 'popup',
               d => 'export',
               e => 'startTime',
               f => 'endTime',
               g => 'id',
               h => 'owner',
               i => 'drawBorder',
               j => 'bgColor',
               k => 'fgColor',
               l => 'mailTo',
               m => 'mailCC',
               n => 'mailBCC',
               o => 'mailText',
               p => 'reminderTimes',
               q => 'reminderTo',
               r => 'category',
               s => 'isTentative',
               t => 'subscriptions',
               u => 'timePeriod',

               A => 'startDate',
               B => 'endDate',
               C => 'period',
               D => 'frequency',
               E => 'monthWeek',
               F => 'monthMonth',
               G => 'exclusions',
               H => 'skipWeekends');

    # Return a list of ascii elements representing an event. Escape newlines.
    sub serialize {
        my $self = shift;
        my @list;

        if ($self->timePeriod) {
            delete $self->{startTime};
            delete $self->{endTime};
        }

        foreach ('a'..'u') {
            my $val = $self->{$map{$_}};
            push @list, ($_, $val) if (defined $val);
        }

        if ($self->isRepeating) {
            foreach ('A'..'F','H') {
                my $val = $self->{'repeatInfo'}->{$map{$_}};
                push @list, ($_, $val) if (defined $val);
            }
            my $val = $self->{'repeatInfo'}->{$map{G}};
            push @list, ('G', $val) if (defined $val and $val->[0]);
        }

        for (my $i=1; $i<@list; $i+=2) {
            if (ref ($list[$i]) eq 'Date') {
                $list[$i] = "$list[$i]";
              # some fields could be lists (period, monthWeek, exclusions)
              # store them separated by whitespace
            } elsif (ref ($list[$i]) eq 'ARRAY') {
                $list[$i] = join ' ', @{$list[$i]};
            } else {
                $list[$i] =~ s/\n/\\n/g;
                $list[$i] =~ s/\r//g; # otherwise it writes carriage returns
            }
        }
        @list;
    }

    sub unserialize {
        my $classname = shift;
        my (%values) = @_;

        my $self = {};
        bless $self, $classname;

        my $val;
        foreach ('a'..'u') {
            next unless defined ($val = $values{$_});
            ($self->{$map{$_}} = $val) =~ s/\\n/\n/g;
        }

        if ($values{'A'}) {     # startDate
            require Calendar::RepeatInfo;
            $self->{repeatInfo} = RepeatInfo->new (@values{'A'..'F','H'});
            if ($values{'G'}) {
                require Calendar::Date;
                my @exclusions = split /\s+/, $values{'G'};
                @exclusions = map {Date->new($_)} @exclusions;
                $self->{repeatInfo}->{'exclusions'} = \@exclusions;
            }
        }
        $self;
    }
}

1;
