The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package RDF::iCalendar::Exporter;

use 5.008;
use base qw[RDF::vCard::Exporter];
use strict;
use warnings;
no warnings qw(uninitialized);

use DateTime;
use MIME::Base64 qw[];
use RDF::iCalendar::Entity;
use RDF::iCalendar::Line;
use RDF::TrineX::Functions
	-shortcuts,
	statement => { -as => 'rdf_statement' },
	iri       => { -as => 'rdf_resource' };
use Scalar::Util qw[blessed];
use URI;

require RDF::vCard;

# kinda constants
sub I    { return 'http://www.w3.org/2002/12/cal/icaltzd#' . shift; }
sub IX   { return 'http://buzzword.org.uk/rdf/icaltzdx#' . shift; }
sub RDF  { return 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' . shift; }
sub V    { return 'http://www.w3.org/2006/vcard/ns#' . shift; }
sub VX   { return 'http://buzzword.org.uk/rdf/vcardx#' . shift; }
sub XSD  { return 'http://www.w3.org/2001/XMLSchema#' . shift; }

sub flatten_node
{
	my $node = shift;
	return $node->value if $node->is_resource || $node->is_literal;
	return $node->as_ntriples;
}

use namespace::clean;

our $VERSION = '0.005';
our $PRODID  = sprintf("+//IDN cpan.org//NONSGML %s v %s//EN", __PACKAGE__, $VERSION);

our %cal_dispatch = (
	);

our %dispatch = (
	IX('contact')    => \&_prop_export_contact,
	I('contact')     => \&_prop_export_contact,
	I('geo')         => \&_prop_export_geo,
	IX('organizer')  => \&_prop_export_Person,
	I('organizer')   => \&_prop_export_Person,
	IX('attendee')   => \&_prop_export_Person,
	I('attendee')    => \&_prop_export_Person,
	I('attach')      => \&RDF::vCard::Exporter::_prop_export_binary,
	I('dtstart')     => \&_prop_export_DateTime,
	I('dtend')       => \&_prop_export_DateTime,
	I('due')         => \&_prop_export_DateTime,
	I('completed')   => \&_prop_export_DateTime,
	I('created')     => \&_prop_export_DateTime,
	I('dtstamp')     => \&_prop_export_DateTime,
	I('last-modified') => \&_prop_export_DateTime,
	IX('location')   => \&_prop_export_location,
	I('location')    => \&_prop_export_location,
	I('rrule')       => \&_prop_export_Recur,
	I('exrule')      => \&_prop_export_Recur,
	I('valarm')      => \&_prop_export_valarm,
	I('freebusy')    => \&_prop_export_freebusy,
	# RELATED-TO
	);

our %list_dispatch = (
	I('exdate')      => ['exdate',     \&_value_export_DateTime],
	I('rdate')       => ['rdate',      \&_value_export_DateTime],
	I('resources')   => ['resources',  \&_value_export_category],
	I('categories')  => ['categories', \&_value_export_category],
	I('category')    => ['categories', \&_value_export_category],
	IX('category')   => ['categories', \&_value_export_category],
	);
	
sub _rebless
{
	my ($self, $thing) = @_;
	if ($thing->isa('RDF::vCard::Line'))
	{
		return bless $thing, 'RDF::iCalendar::Line';
	}
	if ($thing->isa('RDF::vCard::Entity'))
	{
		return bless $thing, 'RDF::iCalendar::Entity';
	}
}

sub _debug
{
#	my ($self, @debug) = @_;
#	printf(@debug);
#	print "\n";
}

sub export_cards # need to really use superclass for these
{
	my ($self, $model, %options) = @_;	
	return RDF::vCard::Exporter->new->export_cards($model, %options);
}

sub export_card # need to really use superclass for these
{
	my ($self, $model, $subject, %options) = @_;	
	return RDF::vCard::Exporter->new->export_card($model, $subject, %options);
}

sub export_calendars
{
	my ($self, $model, %options) = @_;
	$model = rdf_parse($model)
		unless blessed($model) && $model->isa('RDF::Trine::Model');
	
	my @subjects =  $model->subjects(rdf_resource(RDF('type')), rdf_resource(I('Vcalendar')));
	push @subjects, $model->subjects(rdf_resource(I('component')), undef);	
	my %subjects = map { flatten_node($_) => $_ } @subjects;
	
	my @cals;
	foreach my $s (values %subjects)
	{
		push @cals, $self->export_calendar($model, $s, %options);
	}
	
	if ($options{sort})
	{
		return sort { $a->entity_order cmp $b->entity_order } @cals;
	}
	
	return @cals;
}

sub export_calendar
{
	my ($self, $model, $subject, %options) = @_;
	$model = RDF::TrineShortcuts::rdf_parse($model)
		unless blessed($model) && $model->isa('RDF::Trine::Model');
	
	my $ical = RDF::iCalendar::Entity->new( profile=>'VCALENDAR' );
	
	my %categories;
	my $triples = $model->get_statements($subject, undef, undef);
	while (my $triple = $triples->next)
	{
#		next
#			unless (substr($triple->predicate->uri, 0, length(&I)) eq &I or
#					  substr($triple->predicate->uri, 0, length(&IX)) eq &IX);

		if ($triple->predicate->uri eq I('component'))
		{
			$ical->add_component($self->export_component($model, $triple->object));
		}
		elsif (defined $cal_dispatch{$triple->predicate->uri}
		and    ref($cal_dispatch{$triple->predicate->uri}) eq 'CODE')
		{
			my $code = $cal_dispatch{$triple->predicate->uri};
			my $r    = $code->($self, $model, $triple);
			if (blessed($r) and $r->isa('RDF::iCalendar::Line'))
				{ $ical->add($r); }
			elsif (blessed($r) and $r->isa('RDF::iCalendar::Entity'))
				{ $ical->add_component($r); }
		}
		elsif ((substr($triple->predicate->uri, 0, length(&I)) eq &I
		or substr($triple->predicate->uri, 0, length(&IX)) eq &IX))
		{
			$ical->add($self->_prop_export_simple($model, $triple))
				unless $triple->object->is_blank;
		}
	}
			
	$ical->add(
		RDF::iCalendar::Line->new(
			property        => 'version',
			value           => '2.0',
			)
		);
		
	$ical->add(
		RDF::iCalendar::Line->new(
			property        => 'prodid',
			value           => (defined $options{prodid} ? $options{prodid} : $PRODID),
			)
		) unless exists $options{prodid} && !defined $options{prodid};

	$ical->add(
		RDF::iCalendar::Line->new(
			property        => 'source',
			value           => $options{source},
			type_parameters => {value=>'URI'},
			)
		) if defined $options{source};

	return $ical;
}

sub export_component
{
	my ($self, $model, $subject, %options) = @_;
	$model = RDF::TrineShortcuts::rdf_parse($model)
		unless blessed($model) && $model->isa('RDF::Trine::Model');
	
	my $profile = 'VEVENT';
	$profile = 'VTIMEZONE'
		if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vtimezone')));
	$profile = 'VFREEBUSY'
		if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vfreebusy')));
	$profile = 'VALARM'
		if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Valarm')));
	$profile = 'VJOURNAL'
		if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vjournal')));
	$profile = 'VTODO'
		if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vtodo')));
	$profile = 'VEVENT'
		if $model->count_statements($subject, rdf_resource(RDF('type')), rdf_resource(I('Vevent')));
	
	my $c = RDF::iCalendar::Entity->new( profile=>$profile );
	
	$self->_debug("COMPONENT: %s", flatten_node($subject));
	
	my $lists = {};
	
	my $triples = $model->get_statements($subject, undef, undef);
	while (my $triple = $triples->next)
	{
		$self->_debug("  %s %s", $triple->predicate->sse, $triple->object->sse);
	
		if (defined $dispatch{$triple->predicate->uri}
		and ref($dispatch{$triple->predicate->uri}) eq 'CODE')
		{
			$self->_debug("   -> dispatch");
			my $code = $dispatch{$triple->predicate->uri};
			my $r    = $code->($self, $model, $triple);
			if (blessed($r) and $r->isa('RDF::iCalendar::Line'))
				{ $c->add($r); }
			elsif (blessed($r) and $r->isa('RDF::iCalendar::Entity'))
				{ $c->add_component($r); }
		}
		elsif (defined $list_dispatch{$triple->predicate->uri}
		and ref($list_dispatch{$triple->predicate->uri}) eq 'ARRAY')
		{
			$self->_debug("   -> list_dispatch");
			my ($listname, $code) = @{ $list_dispatch{$triple->predicate->uri} };
			push @{ $lists->{$listname} }, $code->($self, $model, $triple);
		}
		elsif ((substr($triple->predicate->uri, 0, length(&I)) eq &I
		or substr($triple->predicate->uri, 0, length(&IX)) eq &IX))
		{
			$self->_debug("   -> default");
			$c->add($self->_prop_export_simple($model, $triple))
				unless $triple->object->is_blank;
		}
		else
		{
			$self->_debug("   -> NO ACTION");
		}
	}
	
	foreach my $listname (keys %$lists)
	{
		$c->add(RDF::iCalendar::Line->new(
			property => $listname,
			value    => [[ sort keys %{{ map { $_ => 1 } @{$lists->{$listname}} }} ]],
			));
	}	
			
	return $c;
}

sub _prop_export_valarm
{
	my ($self, $model, $triple) = @_;
	
	unless ($triple->object->is_literal)
	{
		return $self->export_component($model, $triple->object);
	}
}

sub _prop_export_simple
{
	my ($self, $model, $triple) = @_;
	my $rv = $self->SUPER::_prop_export_simple($model, $triple);
	return $self->_rebless($rv);
}

# iCalendar forces different datetime/date formats than
# the generalised text/directory ones used by vCard...
sub _prop_export_DateTime
{
	my ($self, $model, $triple) = @_;

	my $prop = 'x-data';
	if ($triple->predicate->uri =~ m/([^\#\/]+)$/)
	{
		$prop = $1;
	}
	
	my $val    = undef;
	my $params = undef;

	my ($dt, $has_time) = $self->_node2dt($triple->object, $model);
	my $tz = $dt->time_zone;

	if ($dt and $has_time)
	{
		$params = { value=>'DATE-TIME' };
		
		unless ($tz->is_floating ||
				  $tz->is_utc ||
				  $tz->is_olson )
		{
			$dt = $dt->clone->set_time_zone('UTC');
			$tz = $dt->time_zone;
		}
		
		$val = sprintf('%04d%02d%02dT%02d%02d%02d',
			$dt->year, $dt->month, $dt->day, $dt->hour, $dt->minute, $dt->second);
		
		if ($tz->is_utc)
		{
			$val .= "Z";
		}
		elsif (!$tz->is_floating)
		{
			$params->{tzid} = $tz->name;
		}
	}
	elsif ($dt)
	{
		$params = { value=>'DATE' };
		
		unless ($tz->is_floating ||
				  $tz->is_utc ||
				  $tz->is_olson )
		{
			$dt = $dt->clone->set_time_zone('UTC');
			$tz = $dt->time_zone;
		}
		
		$val = sprintf('%04d%02d%02d',
			$dt->year, $dt->month, $dt->day);
	}

	return RDF::iCalendar::Line->new(
		property        => $prop,
		value           => $val,
		type_parameters => $params,
		);
}

sub _node2dt
{
	my ($self, $node, $model) = @_;
	
	# Shouldn't happen!
	return DateTime->now unless $node->is_literal;
	
	my ($date, $time) = split /T/i, $node->literal_value;	
	my $has_time = (defined $time and length $time);

	my $dt;
	if ($date =~ m'^([0-9]{4})\-?([0-9]{2})\-?([0-9]{2})$')
	{
		$dt = DateTime->new(year => $1, month => $2, day => $3);
	}
	elsif ($date =~ m'^([0-9]{1,4})\-([0-9]{1,2})\-([0-9]{1,2})$')
	{
		$dt = DateTime->new(year => $1, month => $2, day => $3);
	}
	else
	{
		$dt = DateTime->now;
	}
	
	my $zone;
	if ($time =~ /^(.+)(Z|[\+\-]00\:?00])$/i)
	{
		$time = $1;
		$zone = DateTime::TimeZone->new(name => 'UTC');
	}
	elsif ($time =~ /^(.+)([\+\-][0-9][0-9]\:?[0-9][0-9])$/i)
	{
		$time = $1;
		$zone = DateTime::TimeZone->new(name => $2);
	}
	elsif ($node->has_datatype
	and $node->literal_datatype =~ m'^http://www\.w3\.org/2002/12/cal/tzd/(.+)#tz$')
	{
		$zone = DateTime::TimeZone->new(name => $1);
	}
	elsif ($node->has_datatype
	and $node->literal_datatype !~ m'^http://www\.w3\.org/2001/XMLSchema#'
	and defined $model)
	{
		# Some funny datatype; let's try our best!
		my @locations = grep
			{ $_->is_literal }
			$model->objects(
				rdf_resource($node->literal_datatype),
				rdf_resource('http://www.w3.org/2002/12/cal/prod/Ximian_NON_de8f2a9bed573980#location'),
				rdf_resource(RDF('value')),
				rdf_resource(RDFS('label')),
				);
		$zone = DateTime::TimeZone->new(name => $locations[0]->literal_value)
			if @locations;
	}

	$dt->set_time_zone($zone) if $zone;

	if ($time =~ m'^([0-2][0-9])\:?([0-5][0-9])\:?([0-6][0-9](\.[0-9]*)?)?$')
	{
		$dt->set_hour($1)->set_minute($2);
		$dt->set_second($3) if defined $3;
	}
	elsif ($time =~ m'^([0-2]?[0-9])\:([0-5]?[0-9])\:([0-6]?[0-9](\.[0-9]*)?)$')
	{
		$dt->set_hour($1)->set_minute($2)->set_second($3);
	}
	elsif ($time =~ m'^([0-2]?[0-9])\:([0-5]?[0-9])\:?$')
	{
		$dt->set_hour($1)->set_minute($2);
	}

	return ($dt, $has_time);
}

sub _prop_export_contact
{
	my ($self, $model, $triple) = @_;

	if ($triple->object->is_literal)
	{
		return $self->_prop_export_simple($model, $triple);
	}
	
	my $card = $self->export_card($model, $triple->object);
	my $uri  = URI->new('data:');
	$uri->media_type('text/directory');
	$uri->data("$card");

	my $label = '';
	my ($fn)     = $card->get('fn');
	my ($email)  = $card->get('email');
	if ($fn and $email)
	{
		$label = sprintf('%s <%s>',
			$fn->_unescape_value($fn->value_to_string),
			$email->_unescape_value($email->value_to_string),
			);
	}
	elsif ($fn)
	{
		$label = $fn->_unescape_value($fn->value_to_string);
	}
	elsif ($email)
	{
		$label = $email->_unescape_value($email->value_to_string);
	}

	return RDF::iCalendar::Line->new(
		property => 'contact',
		value    => $label,
		type_parameters => {
			altrep   => "\"$uri\"",
			},
		);
}

sub _prop_export_location
{
	my ($self, $model, $triple) = @_;

	$self->_debug("      Location: %s", flatten_node($triple->object));

	if ($triple->object->is_literal)
	{
		$self->_debug("       -> literal");
		return $self->_prop_export_simple($model, $triple);
	}

	if ($model->count_statements(
			$triple->object,
			rdf_resource(RDF('type')),
			rdf_resource(V('VCard')),
			)
	or  $model->count_statements(
			$triple->object,
			rdf_resource(V('fn')),
			undef,
			)
		)
	{
		$self->_debug("       -> vcard");
		my $card = $self->export_card($model, $triple->object);
		return RDF::iCalendar::Line->new(
			property => 'location',
			value    => "$card",
			type_parameters => {
				value => "VCARD",
				},
			);
	}

	elsif ($model->count_statements(
			$triple->object,
			rdf_resource(RDF('type')),
			rdf_resource(V('Address')),
			)
	or $model->count_statements(
			$triple->object,
			rdf_resource(V('locality')),
			undef,
			)
	or $model->count_statements(
			$triple->object,
			rdf_resource(V('street-address')),
			undef,
			)
		)
	{
		$self->_debug("       -> adr");
		my $line = $self->_rebless( $self->_prop_export_adr($model, $triple) );
		$line->{property} = 'location';
		return $line;
	}

	elsif ($model->count_statements(
			$triple->object,
			rdf_resource(RDF('type')),
			rdf_resource(V('Location')),
			)
	or $model->count_statements(
			$triple->object,
			rdf_resource(V('latitude')),
			undef,
			)
		)
	{
		$self->_debug("       -> geo");
		my $line = $self->_rebless( $self->SUPER::_prop_export_geo($model, $triple) );
		$line->{property} = 'location';
		return $line;
	}

	return $self->_prop_export_simple($model, $triple);
}


sub _prop_export_geo
{
	my ($self, $model, $triple) = @_;
	
	if ($triple->object->is_literal)
	{
		return $self->_prop_export_simple($model, $triple);
	}
	elsif ($triple->object->is_resource
	and    $triple->object->uri =~ /^geo:(.+)$/i)
	{
		my $g = $1;
		return RDF::iCalendar::Line->new(
			property => 'geo',
			value    => [ split /[,;]/, $g, 2 ],
			);
	}
	
	my ($lat, $lon);
	{
		my @latitudes = grep
			{ $_->is_literal }
			$model->objects($triple->object, rdf_resource(RDF('first')));
		$lat = $latitudes[0]->literal_value if @latitudes;
		
		my @nodes = grep
			{ !$_->is_literal }
			$model->objects($triple->object, rdf_resource(RDF('next')));
		if (@nodes)
		{
			my @longitudes = grep
				{ $_->is_literal }
				$model->objects($nodes[0], rdf_resource(RDF('first')));
			$lon = $longitudes[0]->literal_value if @longitudes;
		}
	}
	
	return RDF::iCalendar::Line->new(
		property => 'geo',
		value    => [$lat||0, $lon||0],
		);
}

sub _prop_export_Person
{
	my ($self, $model, $triple) = @_;

	if ($triple->object->is_literal)
	{
		return $self->_prop_export_simple($model, $triple);
	}
	
	my $property = {
		I('organizer')  => 'organizer',
		IX('organizer') => 'organizer',
		I('attendee')   => 'attendee',
		IX('attendee')  => 'attendee',
		}->{ $triple->predicate->uri };
	
	my ($name, $email, $role, $partstat, $rsvp, $cutype, %thing_values);
	
	my %thing_meta = (
		'sent-by'        => [map {rdf_resource($_)} IX('sentBy'), I('sent-by')],
		'delegated-to'   => [map {rdf_resource($_)} IX('delegatedTo'), I('delegated-to')],
		'delegated-from' => [map {rdf_resource($_)} IX('delegatedFrom'), I('delegated-from')],
		);
	
	if ($triple->object->is_resource
	and $triple->object->uri =~ /^mailto:.+$/i)
	{
		$email = $triple->object->uri;
	}
	else
	{
		($name) = grep
			{ $_->is_literal }
			$model->objects_for_predicate_list($triple->object, rdf_resource(IX('cn')), rdf_resource(V('fn')));

		($role) = grep
			{ $_->is_literal }
			$model->objects_for_predicate_list($triple->object, rdf_resource(V('role')), rdf_resource(I('role')), rdf_resource(IX('role')));

		($partstat) = grep
			{ $_->is_literal }
			$model->objects_for_predicate_list($triple->object, rdf_resource(I('partstat')), rdf_resource(IX('partstat')));

		($rsvp) = grep
			{ $_->is_literal }
			$model->objects_for_predicate_list($triple->object, rdf_resource(I('rsvp')), rdf_resource(IX('rsvp')));

		($cutype) = grep
			{ $_->is_literal }
			$model->objects_for_predicate_list($triple->object, rdf_resource(VX('kind')), rdf_resource(I('cutype')), rdf_resource(IX('cutype')));

		($email) = $model->objects($triple->object, rdf_resource(V('email')));
		if ($email
		and ($email->is_blank or ($email->is_resource and $email->uri !~ /^mailto:/i)))
		{
			($email) = grep
				{ !$_->is_blank }
				$model->objects($email, rdf_resource(RDF('value')));
		}

		# This bit doesn't just work for sent-by, but also delegated-from/delegated-to
		while (my ($P, $X) = each %thing_meta)
		{
			my ($sentby) = $model->objects_for_predicate_list($triple->object, @$X);
			# if $sentby isn't an email address
			if (!defined $sentby) {}
			elsif ($sentby->is_blank or $sentby->is_resource && $sentby->uri !~ /^mailto:/i)
			{
				# Maybe it's a vcard:Email resource; if so, then get the rdf:value.
				my ($value) = grep
					{ !$_->is_blank }
					$model->objects($triple->object, rdf_resource(RDF('value')));
				if ($value)
				{
					$sentby = $value;
				}
				# If it's not then it might be a vcard:VCard...
				else
				{
					my ($sb_email) = $model->objects($sentby, rdf_resource(V('email')));
					if (!defined $sb_email) {}
					elsif ($sb_email->is_literal or $sb_email->is_resource && $sb_email->uri !~ /^mailto:/i)
					{
						$sentby = $sb_email;
					}
					else
					{
						my ($value) = grep
							{ !$_->is_blank }
							$model->objects($sb_email, rdf_resource(RDF('value')));
						if ($value)
						{
							$sentby = $value;
						}
					}
				}
			}
			
			$thing_values{$P} = $sentby if $sentby;
		}
	}
	
	my %params = ();
	$params{'cn'} = flatten_node($name)
		if defined $name;
	
	foreach my $P (keys %thing_meta)
	{
		$params{$P} = flatten_node($thing_values{$P})
			if defined $thing_values{$P};
	}

	$params{'cutype'} = flatten_node($cutype)
		if (defined $cutype and $property eq 'attendee');
	$params{'partstat'} = flatten_node($partstat)
		if (defined $partstat and $property eq 'attendee');
	$params{'role'} = flatten_node($role)
		if (defined $role and $property eq 'attendee');
	$params{'rsvp'} = flatten_node($rsvp)
		if (defined $rsvp and $property eq 'attendee');

	$params{'value'} = 'CAL-ADDRESS';

	if (!$email)
	{
		$email = $name;
		$params{'value'} = 'TEXT';
	}
	
	return RDF::iCalendar::Line->new(
		property => $property,
		value    => flatten_node($email),
		type_parameters => \%params,
		);
}

sub _value_export_simple
{
	my ($self, $model, $triple) = @_;
	my $rv = $self->_prop_export_simple($model, $triple);
	return $rv->_unescape_value($rv->value_to_string);
}

sub _value_export_DateTime
{
	my ($self, $model, $triple) = @_;
	my $rv = $self->_prop_export_DateTime($model, $triple);
	return $rv->_unescape_value($rv->value_to_string);
}

sub _value_export_category
{
	my ($self, $model, $triple) = @_;

	if ($triple->object->is_literal)
	{
		return uc $triple->object->literal_value;
	}

	my @labels = grep
		{ $_->is_literal }
		$model->objects_for_predicate_list(
			$triple->object,
			rdf_resource('http://www.w3.org/2004/02/skos/core#prefLabel'),
			rdf_resource('http://www.holygoat.co.uk/owl/redwood/0.1/tags/name'),
			rdf_resource('http://www.w3.org/2000/01/rdf-schema#label'),
			rdf_resource('http://www.w3.org/2004/02/skos/core#altLabel'),
			rdf_resource('http://www.w3.org/2004/02/skos/core#notation'),
			rdf_resource(RDF('value')),
			);
	
	if (@labels)
	{
		return uc $labels[0]->literal_value;
	}
	elsif ($triple->object->is_resource)
	{
		return $triple->object->uri;
	}
}

sub _prop_export_Recur
{
	my ($self, $model, $triple) = @_;

	my $prop = 'x-data';
	if ($triple->predicate->uri =~ m/([^\#\/]+)$/)
	{
		$prop = $1;
	}

	if ($triple->object->is_literal)
	{
		return $self->_prop_export_simple($model, $triple);
	}
	
	my (%bits, @bits);
	
	my $iter = $model->get_statements($triple->object, undef, undef);
	while (my $st = $iter->next)
	{
		if ($st->predicate->uri =~ m'^http://www\.w3\.org/2002/12/cal/icaltzd#(.+)$')
		{
			my $p = uc $1;
			my $v = ($p eq 'UNITL') ? $self->_value_export_DateTime($model, $st) : flatten_node($st->object);
			push @{ $bits{$p} }, $v;
		}
	}
	
	while (my ($k, $v) = each %bits)
	{
		push @bits, sprintf('%s=%s', $k, join(',', @$v));
	}
	
	return RDF::iCalendar::Line->new(
		property => $prop,
		value    => [ map { [ split /,/, $_ ] } @bits ],
		type_parameters => { value => 'RECUR' },
		);
}

sub _prop_export_freebusy
{
	my ($self, $model, $triple) = @_;

	if ($triple->object->is_literal)
	{
		return RDF::iCalendar::Line->new(
			property => 'freebusy',
			value    => $triple->object->literal_value,
			type_parameters => { fbtype => 'BUSY' },
			);
	}

	my @values = sort map
		{ $_->literal_value }
		grep
		{ $_->is_literal }
		$model->objects_for_predicate_list(
			$triple->object,
			rdf_resource(RDF('value')),
			);
	
	my ($fbtype) = map
		{ uc $_->literal_value }
		grep
		{ $_->is_literal }
		$model->objects_for_predicate_list(
			$triple->object,
			rdf_resource(I('fbtype')),
			rdf_resource(IX('fbtype')),
			);

	return RDF::iCalendar::Line->new(
		property => 'freebusy',
		value    => [[ @values ]],
		type_parameters => { fbtype => $fbtype || 'BUSY' },
		);

}


1;

__END__

=head1 NAME

RDF::iCalendar::Exporter - export RDF data to iCalendar format

=head1 SYNOPSIS

 use RDF::iCalendar;
 
 my $input    = "http://example.com/calendar-data.ics";
 my $exporter = RDF::iCalendar::Exporter->new;
 
 print $_ foreach $exporter->export_calendars($input);

=head1 DESCRIPTION

This module reads RDF and writes iCalendar files.

This is a subclass of RDF::vCard::Exporter, so it can also export vCards.

=head2 Constructor

=over

=item * C<< new(%options) >>

Returns a new RDF::iCalendar::Exporter object.

There are no valid options at the moment - the hash is reserved
for future use.

=back

=head2 Methods

=over

=item * C<< export_calendars($input, %options) >>

Returns a list of iCalendars found in the input, in no particular order.

The input may be a URI, file name, L<RDF::Trine::Model> or anything else
that can be handled by the C<rdf_parse> method of L<RDF::TrineShortcuts>.

Each item in the list returned is an L<RDF::iCalendar::Entity>, though
that class overloads stringification, so you can just treat each item
as a string mostly.

=item * C<< export_calendar($input, $subject, %options) >>

As per C<export_calendars> but exports just a single calendar.

The subject provided must be an RDF::Trine::Node::Blank or
RDF::Trine::Node::Resource of type icaltzd:Vcalendar.

=item * C<< export_component($input, $subject, %options) >>

Exports a component from a calendar - e.g. a single VEVENT

The subject provided must be an RDF::Trine::Node::Blank or
RDF::Trine::Node::Resource of type icaltzd:Vevent, icaltzd:Vtodo
or similar.

=item * C<< export_cards($input, %options) >>

See L<RDF::vCard::Exporter>.

=item * C<< export_card($input, $subject, %options) >>

See L<RDF::vCard::Exporter>.

=back

=head2 RDF Input

Input is expected to use the newer of the 2005 revision of the W3C's
vCard vocabulary L<http://www.w3.org/TR/rdfcal/>. (Note that even
though this was revised in 2005, the term URIs include "2002" in
them.)

Some extensions from the namespace L<http://buzzword.org.uk/rdf/icaltzdx#>
are also supported. 

=head2 iCalendar Output

The output of this module aims at iCalendar (RFC 2445) compliance.
In the face of weird input data though, (e.g. an DTSTART property that is a
URI instead of a literal) it can pretty easily descend into exporting
junk, non-compliant iCalendars.

The output has barely been tested in any iCalendar-supporting software,
so beware.

=head1 SEE ALSO

L<RDF::iCalendar>.

L<RDF::vCard>, L<HTML::Microformats>, L<RDF::TrineShortcuts>.

L<http://www.w3.org/TR/rdfcal/>.

L<http://www.perlrdf.org/>.

=head1 AUTHOR

Toby Inkster E<lt>tobyink@cpan.orgE<gt>.

=head1 COPYRIGHT

Copyright 2011, 2013 Toby Inkster

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.