#!/usr/bin/perl
#create a .ics file from a text list of events
# Jerry Stratton astoundingscripts.com/ics

use Time::Piece;

$version = '1.0';
$timezone = 'Central';
$baseID = 'calmaker-8035';

our $now = localtime;
our $defaultYear = $now->year;
our $calendarID = '';
our @months = ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');
our %timezones = (
	'Central' => 'America/Chicago',
	'Eastern' => 'America/New_York',
	'Pacific' => 'America/Los_Angeles',
	'Mountain' => 'America/Denver',
);
@timezones = keys %timezones;
$timezoneRE = join('|', @timezones);
$dateRE = '(' . join('|', @months) . ') ([1-9][0-9]?)(-([1-9][0-9]))?(, (2[0-9]{3}))?';
$idRE = '(?i)^slug: *([0-9a-z-]+)$';

help() if grep(/^--help/, @ARGV);

BEGIN {
	package Event;
	use Time::Seconds;
	use List::MoreUtils qw(first_index);
	$maxLineLength = 76;

	sub new {
		my ($class, $title, $zone) = @_;
		my $self = {
			_title => $title,
			_zone => $zone,
		};
		bless $self, $class;
		return $self;
	}

	sub addAddress {
		my ($self, $address) = @_;
		$self->{_address} .= ', ' if $self->{_address} ne '';
		$self->{_address} .= $address;
	}

	sub addDescription {
		my ($self, $text) = @_;
		$self->{_description} .= "\n\n" if $self->{_description} ne '';
		$self->{_description} .= $text;
	}

	sub setDate {
		my ($self, $month, $date, $endDate, $year) = @_;
		my $month = first_index { $_ =~ m/^$month/ } @months;
		$month++;
		$year = $defaultYear if $year eq '';
		$month = "0$month" if length($month) == 1;
		$date = "0$date" if length($date) == 1;
		$self->{_date} = "$year$month$date";
		if ($endDate ne '') {
			my $dayCount = $endDate - $date + 1;
			die("The date for $self->{_title} goes backward from $date to $endDate.\n") if $dayCount < 1;
			$self->setDays($dayCount);
		}
	}

	sub setDays {
		my($self, $days) = @_;
		$self->{_days} = $days;
	}

	sub setDuration {
		my ($self, $duration) = @_;
		$self->{_duration} = $duration;
	}

	sub setID {
		my($self, $id) = @_;
		die("Event IDs require that the calendar also have an ID\n") if $calendarID eq '';
		$self->{_id} = "$calendarID-$id";
		die("Your ID for $self->{_title} is too long ($self->{_id}).\n") if length($self->{_id}) > $maxLineLength;
	}

	sub setLocation {
		my ($self, $location) = @_;
		$self->{_location} = $location;
	}

	sub setTime {
		my ($self, $hour, $minutes, $meridian) = @_;
		$hour += 12 if $hour < 12 && $meridian =~ /^pm$/i;
		$hour = 0 if $hour == 12 && $meridian =~ /^am$/i;
		my $time = $hour*60 + $minutes;
		$self->{_time} = $time;
	}

	sub setURL {
		my ($self, $url) = @_;
		$self->{_link} = $url;
	}

	sub location {
		my ($self) = @_;
		my $location = $self->{_location};
		$location .= "\n" . $self->{_address} if $self->{_address} ne '';
		return $self->makeSafe($location);
	}

	sub startDate {
		my ($self) = @_;
		return $self->formatDateLine('start', $self->{_date}, $self->{_time});
	}

	sub endDate {
		my ($self) = @_;
		return '' if $self->{_duration} eq '' && $self->{_days} eq '';
		die("There is both a duration and an all-day duration for $self->{_title}\n") if $self->{_duration} ne '' && $self->{_days} ne '';
		die("There is a duration but no time for $self->{_title}\n") if $self->{_time} eq '' && $self->{_duration} ne '';

		if (my $time = $self->{_duration}) {
			$time += $self->{_time};
			return $self->formatDateLine('end', $self->{_date}, $time);
		} else {
			my $date = $self->{_date};
			$date =~ m/^(2[0-9]{3})([0-9]{2})([0-9]{2})$/;
			$date = Time::Piece->strptime($date, '%Y%m%d');
			$date += ONE_DAY * $self->{_days};
			$date = $date->strftime('%Y%m%d');
			return $self->formatDateLine('end', $date);
		}
	}

	sub identifier {
		my($self) = @_;

		my $identifier = $self->safe(id);
		if ($identifier eq '') {
			$idCounter++;
			$identifier = "$calendarID-event-$idCounter";
		}
		die("Identifier $identifier for event $self->{_title} duplicates an existing identifier.\n") if grep(/^$identifier$/, @identifiers);
		$identifiers[$#identifiers+1] = $identifier;
		return $identifier;
	}

	sub formatDateLine {
		my ($self, $field, $date, $time) = @_;
		$field = uc($field);
		my $value = $date;
		my $valueType = 'DATE';

		if ($time ne '') {
			$time = $self->formatTime($time);
			$value .= "T${time}";
			$valueType .= '-TIME';
		}
		my $zone = $timezones{$self->{_zone}};
		my $line = "DT${field};TZID=$zone;VALUE=$valueType:$value";
		return $line;
	}

	sub formatTime {
		my ($self, $time) = @_;
		my $hour = int($time/60);
		$minute = $time - $hour*60;
		$hour = "0$hour" if length($hour) == 1;
		$minute = "0$minute" if length($minute) == 1;
		my $time = "${hour}${minute}00";
		return $time;
	}

	sub makeSafe {
		my ($self, $text) = @_;
		$text =~ s/([,;])/\\$1/g;
		$text =~ s/\n/\\n/g;
		return $text;
	}

	sub safe {
		my ($self, $field) = @_;
		my $text = $self->{'_' . $field};
		$text = $self->makeSafe($text);
		return $text;
	}

	sub outputLine {
		my($self, $line) = @_;
		my $prefix = '';
		while ($line ne '') {
			my $subline = substr($line, 0, $maxLineLength);
			$line = substr($line, $maxLineLength);
			print "$prefix$subline\n";
			$prefix = ' ';
		}
	}

	sub close {
		my ($self) = @_;

		#create event
		my $title = $self->safe('title');
		my $description = $self->safe('description');
		my $location = $self->location();
		my $url = $self->safe('link');
		my $date = $self->startDate();
		my $end = $self->endDate();
		my $identifier = $self->identifier();

		print "BEGIN:VEVENT\n";
		print "CLASS:PUBLIC\n";
		$self->outputLine("SUMMARY;LANGUAGE=en-us:$title");
		$self->outputLine("DESCRIPTION:$description") if $description;
		$self->outputLine("LOCATION:$location") if $location;
		$self->outputLine("URL;VALUE=URI:$url") if $url;
		print "UID:$identifier\n";
		print "$date\n";
		print "$end\n" if $end;
		print "END:VEVENT\n";
	}
}

while (<>) {
	chomp;
	next if /^$/;

	if (!$event && !/^## /) {
		if (/^# (.*)$/) {
			$calendar = $1;
		} elsif (/^$timezoneRE$/) {
			$timezone = $_;
			$zonename = $timezones{$timezone};
		} elsif (/^2[0-9]{3}$/) {
			$defaultYear = $_;
		} elsif (/$idRE/) {
			$calendarID = $1;
		} else {
			die("Unknown calendar-wide option $_\n");
		}
	} elsif (/^## (.*)$/) {
		if ($event) {
			$event->close();
		} else {
			startCalendar();
		}
		$event = new Event($1, $timezone);
	} elsif (/^\*\*(.*)\*\*$/) {
		$event->setLocation($1);
	} elsif (/^[1-9][0-9]* .+ (Avenue|Boulevard|Lane|Loop|Place|Road|Street)( [NS]?[EW]?)?$/) {
		$event->addAddress($_);
	} elsif (/^[A-Z][a-zA-Z ]+, [A-Z][A-Z] [0-9]{5}$/) {
		$event->addAddress($_);
	} elsif (/^https:\/\/[^ ]+$/) {
		$event->setURL($_);
	} elsif (/^([12]?[0-9])(:([0-9][0-9]))? ?([AP]M)$/i) {
		$event->setTime($1, $3, $4);
	} elsif (/^([1-9][0-9]*) (hour|minute)s?$/) {
		my $duration = $1;
		$duration *= 60 if $2 eq 'hour';
		$event->setDuration($duration);
	} elsif (/^([1-9][0-9]*) days?$/) {
		$event->setDays($1);
	} elsif (/^$dateRE$/) {
		$event->setDate($1, $2, $4, $6);
	} elsif (/$idRE/) {
		$event->setID($1);
	} else {
		$event->addDescription($_);
	}
}

$event->close();
closeCalendar();

sub startCalendar {
	#get time zone name
	die("Unknown time zone code $timezone\n") if !grep(/^$timezone$/, @timezones);
	my $zonename = $timezones{$timezone};

	#create time zone abbreviation
	my $zoneabbr = substr($timezone, 0, 1);
	$zoneabbr .= substr($now->strftime('%Z'), 1, 1);
	$zoneabbr .= 'T';

	#ensure that the calendar has a name
	$calendar = $ARGV ne '-' ? $ARGV : 'CalMaker Calendar' if $calendar eq '';
	$calendar =~ s/^(.+)\.[^.]+$/$1/;

	#ensure that the calendar has an identifier
	$calendarID = slugify($calendar) if $calendarID eq '';
	$calendarID = "$baseID-$calendarID";
	
	print "BEGIN:VCALENDAR\n";
	print "VERSION:2.0\n";
	print "CALSCALE:GREGORIAN\n";
	print "X-WR-TIMEZONE;VALUE=TEXT:$zoneabbr\n";
	print "X-WR-RELCALID:$calendarID\n";
	print "METHOD:PUBLISH\n";
	print "PRODID:-//hoboes.com/HoboCalGen $version//EN\n";
	print "X-WR-CALNAME:$calendar\n";
	print "BEGIN:VTIMEZONE\n";
	print "TZID:$zonename\n";
	print "END:VTIMEZONE\n";
}

sub closeCalendar {
	print "END:VCALENDAR\n";
}

sub slugify {
	my $text = shift;
	$text = lc($text);
	$text =~ s/[^a-z0-9]/-/g;
	$text =~ s/--+/-/g;
	$text =~ s/^-+//;
	$text =~ s/-+$//;

	return $text;
}

sub help {
	print "$0 [--help] <event text>\n";
	print "Create .ics file from text of events.\n";
	print "Calendar format:\n";
	print "\t# Calendar Title\n";
	print "\ttimezone (", join(', ', @timezones), ")\n";
	print "\tYYYY\n";
	print "\tslug:text\n";
	print "Event format:\n";
	print "\t## Event Title\n";
	print "\t**Location name**\n";
	print "\t1234 Anywhere Lane\n";
	print "\tCity, ST 12345\n";
	print "\tX hours or minutes\n";
	print "\tX days\n";
	print "\tMonth X[-Y][, YYYY]\n";
	print "\tslug:text\n";
	print "\tDescription text.\n";
	exit;
}
