#!/usr/bin/perl

# Author: Florian Schaefer <florian@netego.de>, April 2002
# Feel free to distribute this file under the terms of the GPL.

use POSIX qw(strftime);

@uidl="";
$uidl[0]=0;

#
# Converts a date from dd/mm/yyyy format to yyyymmdd format
# 

sub create_date
{
	my ($raw) = @_;

	$raw =~ s/^([0-9]*)\/([0-9]*)\/([0-9]*)$/$3/;
	if (length($raw) > 4) {chop($raw);}
	$raw .= sprintf("%02i%02i", $2, $1);

	return ($raw);
}

#
# Converts a time in minutes since midnight into hhmm format
# 

sub create_time
{
	my ($mins) = @_;

	my $hour = sprintf("%02i",$mins/60);
	my $min = sprintf("%02i",($mins-60*$hour));

	return ("$hour$min"."00");
}

#
# The function manages to exclude dates on repeated events
#

sub exclude_dates
{
	my ($exdate) = @_;
	my $output ="";
	if ($exdate =~ /,$/) {chop($exdate)};
	$output = "EXDATE\n :$exdate\n";

	return $output;
}

#
# This is the main part: &convert gets an array with the lines
# of one event and the number of lines. It does all the fancy
# work.
#

sub convert
{
	my ($lines, @raw) = @_;
	my $uid, $cont, $tart, $length;
	my $finishdate = $extrastart = 0;
	my $exdate = "";
	$start = "";

	# 
	# This loop tries to get all relevant information
	#
	for (my $pos = 0; $pos < $lines; $pos++)
	{
		if ($raw[$pos] =~ /^Finish /)
		{
			# 
			# find the finish date
			#
			$finishdate = $raw[$pos];
			$finishdate =~ s/ End//;
			$finishdate =~ s/^Finish (.*)/$1/;
			chop($finishdate);
			$finishdate = &create_date($finishdate);
		}
		if ($raw[$pos] =~ /^Start [0-9]/)
		{
			# 
			# an extra start field containing a date
			#
			$extrastart = $raw[$pos];
			$extrastart =~ s/ End//;
			$extrastart =~ s/^Start (.*)/$1/;
			chop($extrastart);
			$extrastart = &create_date($extrastart);
		}

		if ($raw[$pos] =~ /^Deleted /)
		{
			#
			# find dates to be excluded from a repeating event
			#
			my $rawdate = $raw[$pos];
			$rawdate =~ s/ End//;
			$rawdate =~ s/Deleted //;
			$exdate .= &create_date($rawdate).",";
		}
		
		#
		# and finally split the info into two pieces and
		# fill the variables, I know there are hashes, but
		# I don't know how to use them...
		#
		$raw[$pos] =~ s/^(.*) \[(.*)$/$1~$2/;
		($item, $value) = split(/~/, $raw[$pos]);
		$value =~ s/\]$//;
		chop($value);
		if ($item eq "PilotRecordId") {$uid = $value;}
		if ($item eq "Contents") {$cont = $value;}
		if (($item eq "Start") && ($value !=~ /\//)) {$start = $value;}
		if ($item eq "Length") {$length = $value;}
		if ($item eq "Dates") {$date = $value;}
	}

	#
	# the header of each event
	# 
	my $out ="";
	$out  = "BEGIN:VCALENDAR\nVERSION\n :2.0\n";
	$out .= "PRODID\n :PRODID:-//netego.de/NONSGML ical2ics.pl//EN\n";
	$out .= "BEGIN:VEVENT\n";
	$out .= "UID\n :$uid\n";
	$out .= "SUMMARY\n :$cont\n";
	$out .= "CLASS\n :PRIVATE\n";
	$out .= "DESCRIPTION\n :$description\n" if ($description);

	#
	# It is a repeated event - something with days
	#
	if (($date =~ /Days/) && (!($date =~ /WeekDays/)))
	{
		my $interval = $date;
		$interval =~ s/^.* ([0-9]*)$/$1/;
		    
		if ($date =~ /7$/)
		{
			#
			# 7 days, let's make 1 week out of it
			#
			$out .= "X-MOZILLA-RECUR-DEFAULT-UNITS\n :weeks\nX-MOZILLA-RECUR-DEFAULT-INTERVAL\n :1\n";
			$out .= "RRULE\n :";
			$out .= "FREQ=WEEKLY;INTERVAL=1;";
			if ($finishdate) { $out .= "UNTIL=$finishdate\n"; }
			else { chop($out); $out .= "\n"; }
		}
		else
		{
			#
			# seems like a real event for a dayly recurrence
			#
			$out .= "X-MOZILLA-RECUR-DEFAULT-UNITS\n :days\nX-MOZILLA-RECUR-DEFAULT-INTERVAL\n :$interval\n";
			$out .= "RRULE\n :";
			$out .= "FREQ=DAILY;";
			if ($finishdate) { $out .= "UNTIL=$finishdate;"; }
			$out .= "INTERVAL=$interval\n";
		}
		#
		# don't forget to exclude certain dates
		#
		if ($exdate) {$out .= &exclude_dates($exdate);}
	}

	#
	# It is a repeated event - the same with months
	#
	if (($date =~ /Months/) && (!($date =~ /WeekDays/)))
	{
		my $interval = $date;
		$interval =~ s/^.* ([0-9]*)$/$1/;
		    
		if ($date =~ /12$/)
		{
			#
			# 12 month make one year
			#
			$out .= "X-MOZILLA-RECUR-DEFAULT-UNITS\n :years\nX-MOZILLA-RECUR-DEFAULT-INTERVAL\n :1\n";
			$out .= "RRULE\n :";
			$out .= "FREQ=YEARLY;INTERVAL=1;";
			if ($finishdate) { $out .= "UNTIL=$finishdate;"; }
			my $mon = $date;
			$mon =~ s/^.*\/([0-9]+)\/.*$/$1/;
			$out .= "BYMONTH=$mon\n";
		}
		else
		{
			$out .= "X-MOZILLA-RECUR-DEFAULT-UNITS\n :years\nX-MOZILLA-RECUR-DEFAULT-INTERVAL\n :$interval\n";
			$out .= "RRULE\n :";
			$out .= "FREQ=MONTHLY;";
			if ($finishdate) { $out .= "UNTIL=$finishdate;"; }
			$out .= "INTERVAL=$interval\n";
		}
		#
		# don't forget to exclude certain dates
		#
		if ($exdate) {$out .= &exclude_dates($exdate);}
	}
	
	#
	# It is a weekly event, but only some days are selected
	#
	if ($date =~ /WeekDays/)
	{
		$out .= "X-MOZILLA-RECUR-DEFAULT-UNITS\n :weeks\nX-MOZILLA-RECUR-DEFAULT-INTERVAL\n :1\n";
		$out .= "RRULE\n :";
		$out .= "FREQ=WEEKLY;INTERVAL=1;";
		if ($finishdate) { $out .= "UNTIL=$finishdate;"; }
		
		(my $daynames = $date) =~ s/.*WeekDays  (.*) Months.*$/$1/;
		$daynames =~ s/2/MO/;
		$daynames =~ s/3/TU/;
		$daynames =~ s/4/WE/;
		$daynames =~ s/5/TH/;
		$daynames =~ s/6/FR/;
		$daynames =~ s/7/SA/;
		$daynames =~ s/1/SU/;
		$daynames =~ s/ /,/g;
		$out .= "BYDAY=$daynames\n";
			
		#
		# don't forget to exclude certain dates
		#
		if ($exdate) {$out .= &exclude_dates($exdate);}

		#
		# The date of the first event is in another field
		#
		$date = $extrastart;
	}
	
	if ($date =~ /\//)
	{
		#
		# Just in case that date isn't converted yet...
		# The format is slightly different from the one required for
		# &create_date, this is why I don't call it here.
		#
		$date =~ s/^.* ([0-9]*)\/([0-9]*)\/([0-9]*).*$/$3/;
		$date .= sprintf("%02i%02i", $2, $1);
	}

	if ($start)
	{
		#
		# Now just print the start and end time.
		#
		$out = $out."DTSTART\n :".$date."T".&create_time($start)."\n";
		$out = $out."DTEND\n :".$date."T".&create_time($start+$length)."\n";
	}
	else
	{
		#
		# There is no time for the beginning of the event defined, I assume
		# that the event should last all day.
		#
		$out = $out."DTSTART\n ;VALUE=DATE\n :".$date."\n";
	}

	#
	# The RFC requires me to include the time of the object creation
	#
	$date = strftime "%Y%m%dT%H%M%S", localtime;
	$out = $out."DTSTAMP\n :".$date."\n";

	$out = $out."END:VEVENT\nEND:VCALENDAR\n";


	#
	# Remember the UID for later merging
	#
	$uidl[0]++;
	$uidl[$uidl[0]]=$uid;
	
	return ($out);
}

#
# This sub takes an older ical file and merges it to the new one.
# It does this by comparing the stored UID's and adding all entries
# whose ID hasn't been found. As you can see, the new events have
# got a higher priority and can delete newer entries from the old file
#

sub merge_entries
{
	my ($mergefile) =@_;
	my $out = "";        # this string will be added later
	my $event = "";      # the current event being processed
	my $record = 0;      # is true if loop is in an event object
	my $nextoneuid = 0;  # marks the line for the UID
	my $uidfound = 0;    # is true if UID is already in new file

	open MERGEFILE, $mergefile or die "Can't open '$mergefile' for input $!";
	foreach my $line (<MERGEFILE>)
	{
		if ($line =~ /BEGIN:VCALENDAR$/)
		{
			$event = "";
			$record = 1;
		}
		if ($record == 1) {$event.=$line};
		if ($nextoneuid == 1)
		{
			$nextoneuid = 0;
			$uidfound = $line;
			$uidfound =~ s/^ ://;
			chop($uidfound);
		}
		# This way of finding the UID depends on having two lines
		# for field descriptor and value and is therefore Mozilla
		# Calendar specific.
		if ($line =~ /^UID$/) {$nextoneuid=1};
		if ($line =~ /^END:VCALENDAR$/)
		{
			$record = 0;
			my $found = 0;
			for (my $c=1; $c < $uidl[0]+1; $c++)
			{
				$found=1 if ($uidl[$c] eq $uidfound);
			}
			if ($found == 0)
			{
				$out .= $event;
			}
		}
	}
	close (MERGEFILE);
	
	return($out);
}

#
# Here the input file is read and each event is then given to
# &convert for further processing.
#

sub read_palmfile
{
	my ($instream, $outstream) = @_;
	if (($instream  ne "") && ($instream ne "-"))
		{ open STDIN, $instream or die "Can't open '$instream' for input: $!";}
	if ($outstream ne "") 
		{ open STDOUT, ">$outstream" or die "Can't open '$outstream' for output $!";;}
	my $line = "";
	my $pos = 0;
	my @note;
	foreach $line (<STDIN>)
	{
		if ($line =~ /(Note|Appt)/)
		{
			if ($pos != 0)
			{
				print &convert($pos,@note);
			}
			$pos = 0;
			$note[$pos] = $line;
			$pos++;
		}
		elsif ($pos != 0)
		{
			$note[$pos] = $line;
			$pos++;
		}
	}
	close (INFILE);
	print &convert($pos, @note);
	print &merge_entries($mergefile) if ($mergefile);
}

#
# Well, the first sub has to be called somewhere. ;-)
# Most of the stuff below is just option parsing.
#

if (($ARGV[0] eq "--help") || ($ARGV[0] eq "-h"))
{
print << "EOF"

Usage: ical2ics.pl [-d STRING] [-m mergefile] [infile] [outfile]

This little script tries to convert ical calender files produced by read-ical
to a rfc2445 compliant format which can be read by the Mozilla Calendar.

If no filename (or '-' for infile) is given STDIN respectively STDOUT are used.

  -d STRING     Change default description to STRING
  -m mergefile  Merge the outfile with mergefile (the syntax has to be
                similar to the one created by this script), may be the
								outfile itself

Please report bugs or send comments to <florian\@netego.de>.
EOF
}
else
{
	$description = "Imported from Palm Pilot.";
	my $infile = $outfile = ""; # names of the streams
	my $infilenum = 0;          # position of infile in ARGV
	my $outfilenum = 1;         # the same for outfile
	my $argpos = 0;             # pointer to ARGV position
	
	# I hope that this option-vodoo works reliably
	while ($ARGV[$argpos] ne "")
	{
		if ($ARGV[$argpos] eq "-d") 
		{
			$argpos++;
			$description = $ARGV[$argpos];
			$infilenum += 2;
			$outfilenum += 2;
		}
		elsif ($ARGV[$argpos] eq "-m") 
		{
			$argpos++;
			$mergefile = $ARGV[$argpos];
			$infilenum += 2;
			$outfilenum += 2;
		}
		else {$argpos++};
	}
	$infile  = $ARGV[$infilenum];
	$outfile = $ARGV[$outfilenum];
	
	if ($mergefile)
	{
		# mergefile and outfile mustn't be the same, work
		# with a backup copy instead
		my $rnd = int(rand(100000));
		$status = `cp $mergefile /tmp/ical2ics-$rnd.tmp`;
		$mergefile="/tmp/ical2ics-$rnd.tmp";
	}

	&read_palmfile($infile,$outfile,$mergefile);

	unlink $mergefile;
}

# ChangeLog:
#
# 25.07.2002:
# - Switched from the old "X ;MEMBER" fields to the new "X-MOZILLA" properties
# - Events that last all day don't use "MEMBER=AllDay" any more, I just give a
#   DTSTART date and no DTEND
#
# 24.05.2002:
# - New option for merging ('-d') with existing data
#
# 13.05.2002:
# - Added DTSTAMP property
#
# 25.04.2002:
# - The bug in libical and Calendar concerning the EXDATE property have been
#   fixed, therefore I removed the RECURRENCE-ID alternative.
# - Added the -d option
#
# 24.04.2002:
# - First official release
