# RDF::Query::Node::Literal
# -----------------------------------------------------------------------------

=head1 NAME

RDF::Query::Node::Literal - RDF Node class for literals

=head1 VERSION

This document describes RDF::Query::Node::Literal version 2.916.

=cut

package RDF::Query::Node::Literal;

use strict;
use warnings;
no warnings 'redefine';
use base qw(RDF::Query::Node RDF::Trine::Node::Literal);

use DateTime;
use DateTime::Format::W3CDTF;
use RDF::Query::Error;
use Data::Dumper;
use Log::Log4perl;
use Scalar::Util qw(blessed refaddr looks_like_number);
use Carp qw(carp croak confess);

######################################################################

our ($VERSION, $LAZY_COMPARISONS);
BEGIN {
	$VERSION	= '2.916';
}

######################################################################

use overload	'<=>'	=> \&_cmp,
				'cmp'	=> \&_cmp,
				'<'		=> sub { _cmp(@_[0,1], '<') == -1 },
				'>'		=> sub { _cmp(@_[0,1], '>') == 1 },
				'!='	=> sub { _cmp(@_[0,1], '!=') != 0 },
				'=='	=> sub { _cmp(@_[0,1], '==') == 0 },
				'+'		=> sub { $_[0] },
				'""'	=> sub { $_[0]->sse },
			;

my %INSIDE_OUT_DATES;

=head1 METHODS

Beyond the methods documented below, this class inherits methods from the
L<RDF::Query::Node> and L<RDF::Trine::Node::Literal> classes.

=over 4

=cut

sub _cmp {
	my $nodea	= shift;
	my $nodeb	= shift;
	my $op		= shift;
	
	my $l		= Log::Log4perl->get_logger("rdf.query.node.literal");
	$l->debug('literal comparison: ' . Dumper($nodea, $nodeb));
	
	return 1 unless blessed($nodeb);
	return -1 if ($nodeb->isa('RDF::Trine::Node::Nil'));
	return 1 if ($nodeb->isa('RDF::Query::Node::Blank'));
	return 1 if ($nodeb->isa('RDF::Query::Node::Resource'));
	return 1 unless ($nodeb->isa('RDF::Query::Node::Literal'));
	
	my $dta			= $nodea->literal_datatype || '';
	my $dtb			= $nodeb->literal_datatype || '';
	my $datetype	= '^http://www.w3.org/2001/XMLSchema#dateTime';
	my $datecmp		= ($dta =~ $datetype and $dtb =~ $datetype);
	my $numericcmp	= ($nodea->is_numeric_type and $nodeb->is_numeric_type);
	
	if ($datecmp) {
		$l->trace('datecmp');
		my $datea	= $nodea->datetime;
		my $dateb	= $nodeb->datetime;
		if ($datea and $dateb) {
			my $cmp		= eval { DateTime->compare_ignore_floating( $datea, $dateb ) };
			return $cmp unless ($@);
		}
	}
	
	if ($numericcmp) {
		$l->trace('both numeric cmp');
		return 0 if ($nodea->equal( $nodeb ));	# if the nodes are identical, return true (even if the lexical values don't appear to be numeric). i.e., "xyz"^^xsd:integer should equal itself, even though it's not a valid integer.
		return $nodea->numeric_value <=> $nodeb->numeric_value;
	}
	
	{
		$l->trace('other cmp');
		
		if ($nodea->has_language and $nodeb->has_language) {
			$l->trace('both have language');
			my $lc	= lc($nodea->literal_value_language) cmp lc($nodeb->literal_value_language);
			my $vc	= $nodea->literal_value cmp $nodeb->literal_value;
			my $c;
			if ($LAZY_COMPARISONS and ($lc != 0)) {
				$c	= ($vc || $lc);
			} elsif ($lc == 0) {
				$c	= $vc;
			} else {
				$l->debug("Attempt to compare literals with differing languages.");
				throw RDF::Query::Error::TypeError -text => "Attempt to compare literals with differing languages.";
			}
			$l->trace("-> $c");
			return $c;
		} elsif (($nodea->has_datatype and $dta eq 'http://www.w3.org/2001/XMLSchema#string') or ($nodeb->has_datatype and $dtb eq 'http://www.w3.org/2001/XMLSchema#string')) {
			$l->trace("one is xsd:string");
			no warnings 'uninitialized';
			my ($na, $nb)	= sort {
								(blessed($b) and $b->isa('RDF::Query::Node::Literal'))
									? $b->literal_datatype eq 'http://www.w3.org/2001/XMLSchema#string'
									: ($LAZY_COMPARISONS)
										? refaddr($a) <=> refaddr($b)
										: throw RDF::Query::Error::TypeError -text => "Attempt to compare xsd:string with non-literal";
							} ($nodea, $nodeb);
			
			my $c;
			if ($nb->has_language) {
				$c	= -1;
			} elsif (not($nb->has_datatype) or $nb->literal_datatype eq 'http://www.w3.org/2001/XMLSchema#string') {
				$c	= $nodea->literal_value cmp $nodeb->literal_value;
			} elsif ($LAZY_COMPARISONS) {
				return $nodea->as_string cmp $nodeb->as_string;
			} else {
				throw RDF::Query::Error::TypeError -text => "Attempt to compare typed-literal with xsd:string.";
			}
			$l->trace("-> $c");
			return $c;
		} elsif ($nodea->has_datatype and $nodeb->has_datatype) {
			$l->trace("both have datatype");
			my $dc	= $nodea->literal_datatype cmp $nodeb->literal_datatype;
			my $vc	= $nodea->literal_value cmp $nodeb->literal_value;
			my $c;
			
			if ($op eq '!=') {
				throw RDF::Query::Error::TypeError -text => "Attempt to compare (neq) literals with unrecognized datatypes.";
			} else {
				if ($LAZY_COMPARISONS) {
					$c	= ($vc || $dc);
				} elsif ($dc == 0) {
					$c	= $vc;
				} else {
					$l->debug("Attempt to compare literals with different datatypes.");
					throw RDF::Query::Error::TypeError -text => "Attempt to compare literals with differing datatypes.";
				}
				$l->trace("-> $c");
				return $c;
			}
		} elsif ($nodea->has_language or $nodeb->has_language) {
			$l->trace("one has language");
			my $c	= ($nodea->has_language) ? 1 : -1;
			$l->trace("-> $c");
			return $c;
		} elsif ($nodea->has_datatype or $nodeb->has_datatype) {
			$l->trace("one has datatype");
			if ($LAZY_COMPARISONS) {
				my $c	= ($nodea->has_datatype) ? 1 : -1;
				$l->trace("-> $c");
				return $c;
			} else {
				$l->debug("Attempt to compare typed-literal with plain-literal");
				throw RDF::Query::Error::TypeError -text => "Attempt to compare typed-literal with plain-literal";
			}
		} else {
			$l->trace("something else");
			my $vcmp	= $nodea->literal_value cmp $nodeb->literal_value;
			$l->trace("-> $vcmp");
			return $vcmp;
		}
	}
}

=item C<< datetime >>

Returns a DateTime object from the literal if the literal value is in W3CDTF format.

=cut

sub datetime {
	my $self	= shift;
	my $addr	= refaddr( $self );
	if (exists($INSIDE_OUT_DATES{ $addr })) {
		return $INSIDE_OUT_DATES{ $addr };
	} else {
		my $value	= $self->literal_value;
		my $f		= DateTime::Format::W3CDTF->new;
		my $dt		= eval { $f->parse_datetime( $value ) };
		$INSIDE_OUT_DATES{ $addr }	= $dt;
		return $dt;
	}
}

=item C<< as_sparql >>

Returns the SPARQL string for this node.

=cut

sub as_sparql {
	my $self	= shift;
	if ($self->is_numeric_type) {
		return $self->literal_value;
	} else {
		return $self->sse;
	}
}

=item C<< as_hash >>

Returns the query as a nested set of plain data structures (no objects).

=cut

sub as_hash {
	my $self	= shift;
	my $context	= shift;
	my $hash	= {
		type 		=> 'node',
		literal		=> $self->literal_value,
	};
	$hash->{ language }	= $self->literal_value_language if ($self->has_language);
	$hash->{ datatype }	= $self->literal_datatype if ($self->has_datatype);
	return $hash;
}

=item C<< is_simple_literal >>

Returns true if the literal is "simple" -- is a literal without datatype or language.

=cut

sub is_simple_literal {
	my $self	= shift;
	return not($self->has_language or $self->has_datatype);
}

=item C<< is_numeric_type >>

Returns true if the literal is a known (xsd) numeric type.

=cut

sub is_numeric_type {
	my $self	= shift;
	return 0 unless ($self->has_datatype);
	my $type	= $self->literal_datatype;
	if ($type =~ qr<^http://www.w3.org/2001/XMLSchema#(integer|decimal|float|double|non(Positive|Negative)Integer|(positive|negative)Integer|long|int|short|byte|unsigned(Long|Int|Short|Byte))>) {
		return 1;
	} else {
		return 0;
	}
}

=item C<< numeric_value >>

Returns the numeric value of the literal (even if the literal isn't a known numeric type.

=cut

sub numeric_value {
	my $self	= shift;
	if ($self->is_numeric_type) {
		my $value	= $self->literal_value;
		if (looks_like_number($value)) {
			my $v	= 0 + eval "$value";
			return $v;
		} else {
			throw RDF::Query::Error::TypeError -text => "Literal with numeric type does not appear to have numeric value.";
		}
	} elsif (not $self->has_datatype) {
		if (looks_like_number($self->literal_value)) {
			return 0+$self->literal_value;
		} else {
			return;
		}
	} elsif ($self->literal_datatype eq 'http://www.w3.org/2001/XMLSchema#boolean') {
		return ($self->literal_value eq 'true') ? 1 : 0;
	} else {
		return;
	}
}

=item C<< type_list >>

Returns a two-item list suitable for use as the second and third arguments to
RDF::Query::Node::Literal constructor. The two returned values correspond to
literal language tag and literal datatype URI, respectively.

=cut

sub type_list {
	my $self	= shift;
	return ($self->literal_value_language, $self->literal_datatype);
}

sub DESTROY {
	my $self	= shift;
	my $addr	= refaddr($self);
	delete $INSIDE_OUT_DATES{ $addr };
}


1;

__END__

=back

=head1 AUTHOR

 Gregory Todd Williams <gwilliams@cpan.org>

=cut
