#!/usr/local/bin/perl
#
# Convert a netplan file to vCalendar.
#
# Doesn't try to deal with time zones, but assumes netplan file and
# vCalendar file have the same offset. Tested with korganizer:
#
# - CLASS:PRIVATE seems to be ignored by korganizer
# - my .dayplan has some entries without an N(ame) line; they are ignored
#
# Netscape calendar cannot read the generated files (nor those
# generated by korganizer, for that matter), for unknown reasons.
#
# Options:
#
# -o owner-email
#   Add X-ORGANIZER and ATTENDEE attributes with the given e-mail.
#   (Netplan files don't contain the name of the organizer; the file
#   name or directory name serves that purpose, but is not a full
#   e-mail address.)
#
# Author: Bert Bos
# Created: 4 Dec 2000
# Version: 1.0
# $Id: plan2vcs,v 1.2 2000/12/07 23:04:02 bbos Exp $
#
# (Got this from http://www.w3.org/People/Bos/Plan2vcs/plan2vcs)

use Time::Local;
use Getopt::Std;
use strict 'vars';		# Helps avoid bugs...


my $PROG = substr($0, rindex($0, "/") + 1);
my $USAGE = "Usage: $PROG [-o owner\@email] [file [file...]]\n";

my $globalid = 0;		# For generating unique IDs
my $now = time();		# Current time

my @charcode =
  ("=00","=01","=02","=03","=04","=05","=06","=07",
   "=08", "\t","=0A","=0B","=0C","=0D","=0E","=0F",
   "=10","=11","=12","=13","=14","=15","=16","=17",
   "=18","=19","=1A","=1B","=1C","=1D","=1E","=1F",
     " ",  "!", "\"",  "#", "\$",  "%",  "&",  "'",
     "(",  ")",  "*",  "+",  ",",  "-",  ".",  "/",
     "0",  "1",  "2",  "3",  "4",  "5",  "6",  "7",
     "8",  "9",  ":",  ";",  "<","=3D",  ">",  "?",
     "@",  "A",  "B",  "C",  "D",  "E",  "F",  "G",
     "H",  "I",  "J",  "K",  "L",  "M",  "N",  "O",
     "P",  "Q",  "R",  "S",  "T",  "U",  "V",  "W",
     "X",  "Y",  "Z",  "[", "\\",  "]",  "^",  "_",
     "`",  "a",  "b",  "c",  "d",  "e",  "f",  "g",
     "h",  "i",  "j",  "k",  "l",  "m",  "n",  "o",
     "p",  "q",  "r",  "s",  "t",  "u",  "v",  "w",
     "x",  "y",  "z",  "{",  "|",  "}",  "~","=7F",
   "=80","=81","=82","=83","=84","=85","=86","=87",
   "=88","=89","=8A","=8B","=8C","=8D","=8E","=8F",
   "=90","=91","=92","=93","=94","=95","=96","=97",
   "=98","=99","=9A","=9B","=9C","=9D","=9E","=9F",
   "=A0","=A1","=A2","=A3","=A4","=A5","=A6","=A7",
   "=A8","=A9","=AA","=AB","=AC","=AD","=AE","=AF",
   "=B0","=B1","=B2","=B3","=B4","=B5","=B6","=B7",
   "=B8","=B9","=BA","=BB","=BC","=BD","=BE","=BF",
   "=C0","=C1","=C2","=C3","=C4","=C5","=C6","=C7",
   "=C8","=C9","=CA","=CB","=CC","=CD","=CE","=CF",
   "=D0","=D1","=D2","=D3","=D4","=D5","=D6","=D7",
   "=D8","=D9","=DA","=DB","=DC","=DD","=DE","=DF",
   "=E0","=E1","=E2","=E3","=E4","=E5","=E6","=E7",
   "=E8","=E9","=EA","=EB","=EC","=ED","=EE","=EF",
   "=F0","=F1","=F2","=F3","=F4","=F5","=F6","=F7",
   "=F8","=F9","=FA","=FB","=FC","=FD","=FE","=FF");

my @weekday = ("MO", "TU", "WE", "TH", "FR", "SA", "SU");


# nextid -- return a pretty unique identifier
sub nextid() { "p-" . $now . "-" . $$ . "-" . $globalid++ }


# isodate -- return a date & time string in ISO format for a time value
sub isodate($) {
  my ($time) = @_;

  my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = localtime($time);
  return sprintf("%04d%02d%02dT%02d%02d%02d",
		 1900 + $year, $mon + 1, $mday, $hour, $min, $sec);
}


# encode -- encode a string as quoted-printable
sub encode($) {
  my ($s) = @_;

  $s =~ s/\n/\r\n/sgo;	# Canonical RFC 822 line endings
  $s = join("", map($charcode[$_], unpack("C*", $s)));
  $s =~ s/ $/=20/so;		# Encode space at end of line
  $s =~ s/\t$/=09/so;		# Encode tab at end of line

  $s =~ s/(=0D=0A)/$1=\n/go;	# Some pretty printing...
  $s =~ s/=\n\z//so;		# Remove last empty line

  $s =~ s/(.{65,68}(=..|[^=]{3}))/$1=\n/go; # Split long lines

  return $s;
}


# print_entry -- output an event in vcalendar format
sub print_entry($$) {
  my ($owner, $entry) = @_;

  print "BEGIN:VEVENT\n";
  print "DTSTART:", isodate($entry->{START}), "\n";
  print "DTEND:", isodate($entry->{END}), "\n" if (defined $entry->{END});
  print "DCREATED:", isodate($now), "\n";
  print "UID:", nextid(), "\n";
  print "LAST-MODIFIED:", isodate($now), "\n";
  print "X-ORGANIZER:", $owner, "\n" if (defined $owner);
  print "ATTENDEE;ROLE=OWNER:", $owner, "\n" if (defined $owner);
  print "SUMMARY:", $entry->{NAME}, "\n";
  print "CLASS:PRIVATE\n" if ($entry->{PRIVATE});
  print "PRIORITY:", $entry->{COLOR}, "\n"; # Is this meaningful?
  print "DESCRIPTION;ENCODING=QUOTED-PRINTABLE:=\n",
    encode($entry->{MEMO}), "\n" if (defined $entry->{MEMO});

  if (defined $entry->{REPEATS}) {
    print "RRULE:";

    if ($entry->{REP_DAYS} != 0) {
      # Repeat every N days
      print "D", $entry->{REP_DAYS};

    } elsif ($entry->{REP_WEEKDAYS} != 0 && $entry->{REP_WEEKDAYS} < 0200) {
      # Repeat on certain named days of the week
      # Bit 0=Mo..7=Su
      print "W1";
      for (my $i = 0; $i < 7; $i++) {
	print " ", $weekday[$i] if ($entry->{REP_WEEKDAYS} & (1 << $i));
      }

    } elsif ($entry->{REP_WEEKDAYS} >= 0200) {
      # Repeat on certain named days of the week in certain weeks of the month
      # Bit 8=1st..12=5th, 13=last week
      print "MP1";
      for (my $j = 0; $j < 5; $j++) {
	if ($entry->{REP_WEEKDAYS} & (0200 << $j)) {
	  print " ", $j + 1, "+";
	  for (my $i = 0; $i < 7; $i++) {
	    print " ", $weekday[$i] if ($entry->{REP_WEEKDAYS} & (1 << $i));
	  }
	}
      }
      if ($entry->{REP_WEEKDAYS} & 010000) {
	# Repeat on named days of the week every last week of the month
	print " 1-";
	for (my $i = 0; $i < 7; $i++) {
	  print " ", $weekday[$i] if ($entry->{REP_WEEKDAYS} & (1 << $i));
	}
      }

    } elsif ($entry->{REP_MONTHDAYS} != 0) {
      # Repeat on certain days every month
      print "MD1";
      for (my $i = 1; $i <= 31; $i++) {
	print " ", $i if ($entry->{REP_MONTHDAYS} & (1 << $i));
      }
      # Repeat on the last day of every month
      print " LD" if ($entry->{REP_MONTHDAYS} & 1);

    } elsif ($entry->{REP_YEARLY}) {
      # Repeat on the same day every year
      print "YD1";
    }

    if ($entry->{REP_END} > $entry->{START}) {
      # There is an end date for the recurrence
      print " ", isodate($entry->{REP_END})
    } else {
      print " #0";		# Infinitely many times
    }
    print "\n";
  }

  if ($entry->{WARN1} != $entry->{START}) {
    #print "DALARM:", isodate($entry->{WARN1}), ";1;", $entry->{NAME}, "\n";
    print "DALARM:", isodate($entry->{WARN1}), ";1;beep!\n";
    # To do: what do we do with WARN2?
  }

  print "END:VEVENT\n\n";
}


# Main body

my $entry = {};
my $plan_epoch = timegm(0, 0, 0, 1, 0, 1970);
my %options;

getopts('o:', \%options) || die $USAGE;
my $owner = defined $options{o} ? $options{o} : undef;

print "BEGIN:VCALENDAR\n";
print "VERSION:1.0\n";
print "PRODID:-//Bert Bos//NONSGML plan2vcs//EN\n";
print "\n";

while (<>) {

  if (/^[0-9]/) {		# A date, starts a new entry

    print_entry($owner, $entry) if (defined $entry->{NAME});
    $entry = {};

    # Parse the dates, times and status
    /^(\d+)\/(\d+)\/(\d+)	# date
      \s+(\d+):(\d+):(\d+)	# time
	\s+(\d+):(\d+):(\d+)	# length
	  \s+(\d+):(\d+):(\d+)	# first warning
	    \s+(\d+):(\d+):(\d+) # second warning
	      \s+(\S+)		# status
		\s+(\d)		# color
		  .*/xo		# some other color?
		    or die "$PROG: unparsable line: $_";

    if ($4 eq "99") {		# Whole day (no start time)
      $entry->{WHOLE_DAY} = 1;
      $entry->{START} = timelocal(0, 0, 0, $2, $1 - 1, $3);
    } else {
      $entry->{START} = timelocal($6, $5, $4, $2, $1 - 1, $3);
    }
    if (my $duration = 3600 * $7 + 60 * $8 + $9) {
      $entry->{END} = $entry->{START} + $duration;
    }
    $entry->{WARN1} = $entry->{START} - 3600 * $10 - 60 * $11 - $12;
    $entry->{WARN2} = $entry->{START} - 3600 * $13 - 60 * $14 - $15;
    my $status = $16;
    $entry->{COLOR} = $17;
    $entry->{OTHER} = $18;

    $entry->{SUSPENDED} =	($status =~ /^S/);
    $entry->{PRIVATE} =		($status =~ /^.P/);
    $entry->{NOALARM} =		($status =~ /^..N/);
    # $entry->{NO_MONTHVIEW1} =	($status =~ /^...M/);
    # $entry->{NO_YEARVIEW} =	($status =~ /^....Y/);
    # $entry->{NO_WEEKVIEW} =	($status =~ /^.....W/);
    # $entry->{NO_OVERVIEW} =	($status =~ /^......O/);
    # $entry->{NO_MONTHVIEW2} =	($status =~ /^.......M/);
    # $entry->{NO_OTHER} =	($status =~ /^........D/);

  } elsif (/^R/) {		# Event is repeated

    /^R\t(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)/
      or die "$PROG: cannot parse line: $_\n";
    $entry->{REPEATS} = 1;
    $entry->{REP_DAYS} = $1/86400; # Repeat every N days
    $entry->{REP_END} = $plan_epoch + $2; # Stop repeating on this date
    $entry->{REP_WEEKDAYS} = $3; # Bitmap Su, Mo,..., Sa
    $entry->{REP_MONTHDAYS} = $4; # Bitmap 1, 2,..., 31
    $entry->{REP_YEARLY} = $5;	# 0 = no, 1 = yes

    # Let weekdays and monthdays override the day count
    $entry->{REP_DAYS} = 0 if ($entry->{REP_WEEKDAYS} != 0);
    $entry->{REP_DAYS} = 0 if ($entry->{REP_MONTHDAYS} != 0);

    # Check if it is a multi-day event, rather than a repeated one
    if ($entry->{REP_DAYS} == 1 && defined $entry->{WHOLE_DAY}) {
      $entry->{REPEATS} = undef;
      $entry->{END} = $entry->{REP_END};
    }

  } elsif (/^N/) {		# Name of the event

    /^N\t(.*)/ or die "$PROG: missing tab in line: $_\n";
    $entry->{NAME} = $1;

  } elsif (/^M/) {		# Memo

    /^M\t(.*)/ or die "$PROG: missing tab in line: $_\n";
    $entry->{MEMO} .= $1 . "\n";

  } elsif (/^S/) {		# ?? Inserted by Pilot conduit

  } elsif (/^plan/) {		# Version

    /^plan V1.8.2/ or warn "$PROG: only tested for V1.8.2, but input is $_\n";

  } elsif (/^[oOtelayPpmu]/) {	# Various options

    # To do: might pick up the time zone info...

  } else {

    warn "$PROG: ignoring unrecognized line: $_\n";

  }
}

# Output the last parsed entry, if any
print_entry($owner, $entry) if (defined $entry->{NAME});

print "END:VCALENDAR\n";
