use v5.14;
use warnings;

=head1 NAME

Attean::SimpleQueryEvaluator - Simple query evaluator

=head1 VERSION

This document describes Attean::SimpleQueryEvaluator version 0.029

=head1 SYNOPSIS

  use v5.14;
  use Attean;
  my $algebra = Attean->get_parser('SPARQL')->parse('SELECT * WHERE { ... }');
  my $active_graph = Attean::IRI->new('http://example.org/');
  my $e = Attean::SimpleQueryEvaluator->new( model => $model );
  my $iter = $e->evaluate( $algebra, $active_graph );

=head1 DESCRIPTION

The Attean::SimpleQueryEvaluator class implements a simple query evaluator that,
given an L<Attean::API::Algebra|Attean::API::Query> and a L<Attean::API::Model>
object, evaluates the query represented by the algebra using data from the
model, and returns a query result.

=head1 ATTRIBUTES

=over 4

=cut

use Attean::Algebra;
use Attean::Expression;

package Attean::SimpleQueryEvaluator 0.029 {
	use Moo;
	use Encode qw(encode);
	use Attean::RDF;
	use LWP::UserAgent;
	use Scalar::Util qw(blessed);
	use List::Util qw(all any reduce);
	use Types::Standard qw(ConsumerOf InstanceOf Bool);
	use URI::Escape;
	use namespace::clean;

=item C<< model >>

The L<Attean::API::Model> object used for query evaluation.

=cut

	has 'model' => (is => 'ro', isa => ConsumerOf['Attean::API::Model'], required => 1);
	
=item C<< default_graph >>

The L<Attean::API::IRI> object representing the default graph in the C<< model >>.
The default graph will be excluded from enumeration of graph names for query
features such as C<< GRAPH ?g {} >>.

=cut

	has 'default_graph'	=> (is => 'ro', isa => ConsumerOf['Attean::API::IRI'], required => 1);

	has 'user_agent' => (is => 'rw', isa => InstanceOf['LWP::UserAgent'], default => sub { my $ua = LWP::UserAgent->new(); $ua->agent("Attean/$Attean::VERSION " . $ua->_agent); $ua });
	
	has 'ground_blanks' => (is => 'rw', isa => Bool, default => 0);
	
=back

=head1 METHODS

=over 4

=item C<< evaluate( $algebra, $active_graph ) >>

Returns an L<Attean::API::Iterator> object with results produced by evaluating
the query C<< $algebra >> against the evaluator's C<< model >>, using the
supplied C<< $active_graph >>.

=cut

	sub evaluate {
		my $self			= shift;
		my $algebra			= shift;
		my $active_graph	= shift || Carp::confess "No active-graph passed to Attean::SimpleQueryEvaluator->evaluate";
		
		Carp::confess "No algebra passed for evaluation" unless ($algebra);
		
		my $expr_eval	= Attean::SimpleQueryEvaluator::ExpressionEvaluator->new( evaluator => $self );

		my @children	= @{ $algebra->children };
		my ($child)		= $children[0];
		if ($algebra->isa('Attean::Algebra::Query') or $algebra->isa('Attean::Algebra::Update')) {
			return $self->evaluate($algebra->child, $active_graph, @_);
		} elsif ($algebra->isa('Attean::Algebra::BGP')) {
			my @triples	= @{ $algebra->triples };
			if (scalar(@triples) == 0) {
				my $b	= Attean::Result->new( bindings => {} );
				return Attean::ListIterator->new(variables => [], values => [$b], item_type => 'Attean::API::Result');
			} else {
				my @iters;
				my @new_vars;
				my %blanks;
				foreach my $t (@triples) {
					my $q		= $t->as_quad_pattern($active_graph);
					my @values;
					foreach my $v ($q->values) {
						if (not($self->ground_blanks) and $v->does('Attean::API::Blank')) {
							unless (exists $blanks{$v->value}) {
								$blanks{$v->value}	= Attean::Variable->new();
								push(@new_vars, $blanks{$v->value}->value);
							}
							push(@values, $blanks{$v->value});
						} else {
							push(@values, $v);
						}
					}
					push(@iters, $self->model->get_bindings( @values ));
				}
				while (scalar(@iters) > 1) {
					my ($lhs, $rhs)	= splice(@iters, 0, 2);
					unshift(@iters, $lhs->join($rhs));
				}
				return shift(@iters)->map(sub { shift->project_complement(@new_vars) });
			}
		} elsif ($algebra->isa('Attean::Algebra::Distinct') or $algebra->isa('Attean::Algebra::Reduced')) {
			my %seen;
			my $iter	= $self->evaluate( $child, $active_graph );
			return $iter->grep(sub { return not($seen{ shift->as_string }++); });
		} elsif ($algebra->isa('Attean::Algebra::Extend')) {
			my $child	= $algebra;
			my @extends;
			my %extends;
			while ($child->isa('Attean::Algebra::Extend')) {
				my $expr			= $child->expression;
				my $var				= $child->variable->value;
				$extends{ $var }	= $expr;
				unshift(@extends, $var);
				($child)			= @{ $child->children };
			}
			return $self->evaluate( $child, $active_graph )->map(sub {
				my $r	= shift;
				my %extension;
				my %row_cache;
				foreach my $var (@extends) {
					my $expr	= $extends{ $var };
					my $val		= $expr_eval->evaluate_expression( $expr, $r, $active_graph, \%row_cache );
# 					warn "Extend error: $@" if ($@);
					$r	= Attean::Result->new( bindings => { $var => $val } )->join($r) if ($val);
				}
				return $r;
			});
		} elsif ($algebra->isa('Attean::Algebra::Filter')) {
			# TODO: Merge adjacent filter evaluation so that they can share a row_cache hash (as is done for Extend above)
			my $expr	= $algebra->expression;
			my $iter	= $self->evaluate( $child, $active_graph );
			return $iter->grep(sub {
				my $t	= $expr_eval->evaluate_expression( $expr, shift, $active_graph, {} );
# 				if ($@) { warn "Filter evaluation: $@\n" };
				return ($t ? $t->ebv : 0);
			});
		} elsif ($algebra->isa('Attean::Algebra::OrderBy')) {
			my $iter	= $self->evaluate( $child, $active_graph );
			my @rows	= $iter->elements;
			my @cmps	= @{ $algebra->comparators };
			my @exprs	= map { $_->expression } @cmps;
			my @dirs	= map { $_->ascending } @cmps;
			my @sorted	= map { $_->[0] } sort {
				my ($ar, $avalues)	= @$a;
				my ($br, $bvalues)	= @$b;
				my $c	= 0;
				foreach my $i (0 .. $#cmps) {
					my ($av, $bv)	= map { $_->[$i] } ($avalues, $bvalues);
					$c		= $av ? $av->compare($bv) : 1;
					$c		*= -1 if ($dirs[$i] == 0);
					last unless ($c == 0);
				}
				$c
			} map { my $r = $_; [$r, [map { $expr_eval->evaluate_expression( $_, $r, $active_graph, {} ) } @exprs]] } @rows;
			return Attean::ListIterator->new( values => \@sorted, item_type => $iter->item_type, variables => $iter->variables);
		} elsif ($algebra->isa('Attean::Algebra::Service')) {
			my $endpoint	= $algebra->endpoint->value;
			my ($pattern)	= @{ $algebra->children };
			my $sparql		= Attean::Algebra::Project->new( variables => [ map { variable($_) } $pattern->in_scope_variables ], children => [ $pattern ] )->as_sparql;
			my $url			= $endpoint . '?query=' . uri_escape($sparql); # TODO: fix for cases where $endpoint already contains query parameters
			my $ua			= $self->user_agent;
			my $req			= HTTP::Request->new('GET', $url );
			my $response	= $ua->request($req);
			if (blessed($response) and $response->is_success) {
				my $type	= $response->header('Content-Type');
				my $pclass	= Attean->get_parser(media_type => $type) or die "No parser for media type: $type";
				my $parser	= $pclass->new();
				my $xml		= $response->decoded_content;
				my $bytes	= encode('UTF-8', $xml, Encode::FB_CROAK);
				return $parser->parse_iter_from_bytes($bytes);
			} elsif ($algebra->silent) {
				my $b	= Attean::Result->new( bindings => {} );
				return Attean::ListIterator->new(variables => [], values => [$b], item_type => 'Attean::API::Result');
			} else {
				die "Service error: " . $response->status_line;
			}
		} elsif ($algebra->isa('Attean::Algebra::Graph')) {
			my $graph	= $algebra->graph;
			return $self->evaluate($child, $graph) if ($graph->does('Attean::API::Term'));
			
			my @iters;
			my $graphs	= $self->model->get_graphs();
			my %vars;
			while (my $g = $graphs->next) {
				next if ($g->value eq $self->default_graph->value);
				my $gr	= Attean::Result->new( bindings => { $graph->value => $g } );
				my $iter	= $self->evaluate($child, $g)->map(sub { if (my $result = shift->join($gr)) { return $result } else { return } });
				foreach my $v (@{ $iter->variables }) {
					$vars{$v}++;
				}
				push(@iters, $iter);
			}
			return Attean::IteratorSequence->new( variables => [keys %vars], iterators => \@iters, item_type => 'Attean::API::Result' );
		} elsif ($algebra->isa('Attean::Algebra::Group')) {
			my @groupby	= @{ $algebra->groupby };
			my $iter	= $self->evaluate($child, $active_graph);
			my %groups;
			while (my $r = $iter->next) {
				my %vars;
				my %row_cache;
				my @group_terms	= map { $expr_eval->evaluate_expression( $_, $r, $active_graph, \%row_cache ) } @groupby;
				my $key			= join(' ', map { blessed($_) ? $_->as_string : '' } @group_terms);
				my %group_bindings;
				foreach my $i (0 .. $#group_terms) {
					my $v	= $groupby[$i];
					if (blessed($v) and $v->isa('Attean::ValueExpression') and $v->value->does('Attean::API::Variable') and $group_terms[$i]) {
						$group_bindings{$v->value->value}	= $group_terms[$i];
					}
				}
				$groups{$key}	= [Attean::Result->new( bindings => \%group_bindings ), []] unless (exists($groups{$key}));
				push(@{ $groups{$key}[1] }, $r);
			}
			my @keys	= keys %groups;
			$groups{''}	= [Attean::Result->new( bindings => {} ), []] if (scalar(@keys) == 0);
			my $aggs	= $algebra->aggregates;
			my @results;
			my %vars;
			foreach my $key (keys %groups) {
				my %row_cache;
				my ($binding, $rows)	= @{ $groups{$key} };
				my $count	= scalar(@$rows);
				my %bindings;
				foreach my $i (0 .. $#{ $aggs }) {
					my $name	= $aggs->[$i]->variable->value;
					my $term	= $expr_eval->evaluate_expression( $aggs->[$i], $rows, $active_graph, {} );
# 					warn "AGGREGATE error: $@" if ($@);
					$vars{$name}++;
					$bindings{ $name } = $term if ($term);
				}
				push(@results, Attean::Result->new( bindings => \%bindings )->join($binding));
			}
			return Attean::ListIterator->new(variables => [keys %vars], values => \@results, item_type => 'Attean::API::Result');
		} elsif ($algebra->isa('Attean::Algebra::Join')) {
			my ($lhs, $rhs)	= map { $self->evaluate($_, $active_graph) } @children;
			return $lhs->join($rhs);
		} elsif ($algebra->isa('Attean::Algebra::LeftJoin')) {
			my $expr	= $algebra->expression;
			my ($lhs_iter, $rhs_iter)	= map { $self->evaluate($_, $active_graph) } @children;
			my @rhs		= $rhs_iter->elements;
			my @results;
			my %vars	= map { $_ => 1 } (@{ $lhs_iter->variables }, @{ $rhs_iter->variables });
			while (my $lhs = $lhs_iter->next) {
				my $joined	= 0;
				foreach my $rhs (@rhs) {
					if (my $j = $lhs->join($rhs)) {
						if ($expr_eval->evaluate_expression( $expr, $j, $active_graph, {} )->ebv) {
							$joined++;
							push(@results, $j);
						}
					}
				}
				push(@results, $lhs) unless ($joined);
			}
			return Attean::ListIterator->new( variables => [keys %vars], values => \@results, item_type => 'Attean::API::Result');
		} elsif ($algebra->isa('Attean::Algebra::Minus')) {
			my ($lhsi, $rhs)	= map { $self->evaluate($_, $active_graph) } @children;
			my @rhs				= $rhs->elements;
			my @results;
			while (my $lhs = $lhsi->next) {
				my @compatible;
				my @disjoint;
				RHS: foreach my $rhs (@rhs) {
					if (my $j = $lhs->join($rhs)) {
						push(@compatible, 1);
					} else {
						push(@compatible, 0);
					}
					
					my $intersects	= 0;
					my %lhs_dom	= map { $_ => 1 } $lhs->variables;
					foreach my $rvar ($rhs->variables) {
						if (exists $lhs_dom{$rvar}) {
							$intersects	= 1;
						}
					}
					push(@disjoint, not($intersects));
				}
				
				my $count	= scalar(@rhs);
				my $keep	= 1;
				foreach my $i (0 .. $#rhs) {
					$keep	= 0 unless ($compatible[$i] == 0 or $disjoint[$i] == 1);
				}
				
				push(@results, $lhs) if ($keep);
			}
			return Attean::ListIterator->new( variables => $lhsi->variables, values => \@results, item_type => 'Attean::API::Result');
		} elsif ($algebra->isa('Attean::Algebra::Path')) {
			my $s			= $algebra->subject;
			my $path		= $algebra->path;
			my $o			= $algebra->object;
			my @children	= @{ $path->children };
			my ($child)		= $children[0];
			
			return $self->model->get_bindings( $s, $path->predicate, $o, $active_graph ) if ($path->isa('Attean::Algebra::PredicatePath'));
			if ($path->isa('Attean::Algebra::InversePath')) {
				my $path	= Attean::Algebra::Path->new( subject => $o, path => $child, object => $s );
				return $self->evaluate( $path, $active_graph );
			} elsif ($path->isa('Attean::Algebra::AlternativePath')) {
				my @children	= @{ $path->children };
				my @algebras	= map { Attean::Algebra::Path->new( subject => $s, path => $_, object => $o ) } @children;
				my @iters		= map { $self->evaluate($_, $active_graph) } @algebras;
				return Attean::IteratorSequence->new( iterators => \@iters, item_type => $iters[0]->item_type, variables => [$algebra->in_scope_variables] );
			} elsif ($path->isa('Attean::Algebra::NegatedPropertySet')) {
				my $preds	= $path->predicates;
				my %preds	= map { $_->value => 1 } @$preds;
				my $filter	= $self->model->get_quads($s, undef, $o, $active_graph)->grep(sub {
					my $q	= shift;
					my $p	= $q->predicate;
					return not exists $preds{ $p->value };
				});
				my %vars;
				$vars{subject}	= $s->value if ($s->does('Attean::API::Variable'));
				$vars{object}	= $o->value if ($o->does('Attean::API::Variable'));
				return $filter->map(sub {
					my $q	= shift;
					return unless $q;
					my %bindings	= map { $vars{$_} => $q->$_() } (keys %vars);
					return Attean::Result->new( bindings => \%bindings );
				}, 'Attean::API::Result', variables => [values %vars]);
			} elsif ($path->isa('Attean::Algebra::SequencePath')) {
				if (scalar(@children) == 1) {
					my $path	= Attean::Algebra::Path->new( subject => $s, path => $children[0], object => $o );
					return $self->evaluate($path, $active_graph);
				} else {
					my @paths;
					my $first		= shift(@children);
					my $join		= Attean::Variable->new();
					my @new_vars	= ($join->value);
					push(@paths, Attean::Algebra::Path->new( subject => $s, path => $first, object => $join ));
					foreach my $i (0 .. $#children) {
						my $newjoin	= Attean::Variable->new();
						my $obj		= ($i == $#children) ? $o : $newjoin;
						push(@new_vars, $newjoin->value);
						push(@paths, Attean::Algebra::Path->new( subject => $join, path => $children[$i], object => $obj ));
						$join	= $newjoin;
					}
					
					while (scalar(@paths) > 1) {
						my ($l, $r)	= splice(@paths, 0, 2);
						unshift(@paths, Attean::Algebra::Join->new( children => [$l, $r] ));
					}
					return $self->evaluate(shift(@paths), $active_graph)->map(sub { shift->project_complement(@new_vars) });
				}
			} elsif ($path->isa('Attean::Algebra::ZeroOrMorePath') or $path->isa('Attean::Algebra::OneOrMorePath')) {
				if ($s->does('Attean::API::Term') and $o->does('Attean::API::Variable')) {
					my $v	= {};
					if ($path->isa('Attean::Algebra::ZeroOrMorePath')) {
						$self->_ALP($active_graph, $s, $child, $v);
					} else {
						my $iter	= $self->_eval($active_graph, $s, $child);
						while (my $n = $iter->next) {
							$self->_ALP($active_graph, $n, $child, $v);
						}
					}
					my @results	= map { Attean::Result->new( bindings => { $o->value => $_ } ) } (values %$v);
					return Attean::ListIterator->new(variables => [$o->value], values => \@results, item_type => 'Attean::API::Result');
				} elsif ($s->does('Attean::API::Variable') and $o->does('Attean::API::Variable')) {
					my $nodes	= $self->model->graph_nodes( $active_graph );
					my @results;
					while (my $t = $nodes->next) {
						my $tr		= Attean::Result->new( bindings => { $s->value => $t } );
						my $p		= Attean::Algebra::Path->new( subject => $t, path => $path, object => $o );
						my $iter	= $self->evaluate($p, $active_graph);
						while (my $r = $iter->next) {
							push(@results, $r->join($tr));
						}
					}
					my %vars	= map { $_ => 1 } ($s->value, $o->value);
					return Attean::ListIterator->new(variables => [keys %vars], values => \@results, item_type => 'Attean::API::Result');
				} elsif ($s->does('Attean::API::Variable') and $o->does('Attean::API::Term')) {
					my $pp	= Attean::Algebra::InversePath->new( children => [$child] );
					my $p	= Attean::Algebra::Path->new( subject => $o, path => $pp, object => $s );
					return $self->evaluate($p, $active_graph);
				} else { # Term ZeroOrMorePath(path) Term
					my $v	= {};
					$self->_ALP($active_graph, $s, $child, $v);
					my @results;
					foreach my $v (values %$v) {
						return Attean::ListIterator->new(variables => [], values => [Attean::Result->new()], item_type => 'Attean::API::Result')
							if ($v->equals($o));
					}
					return Attean::ListIterator->new(variables => [], values => [], item_type => 'Attean::API::Result');
				}
			} elsif ($path->isa('Attean::Algebra::ZeroOrOnePath')) {
				my $path	= Attean::Algebra::Path->new( subject => $s, path => $child, object => $o );
				my @iters;
				my %seen;
				push(@iters, $self->evaluate( $path, $active_graph )->grep(sub { return not($seen{shift->as_string}++); }));
				push(@iters, $self->_zeroLengthPath($s, $o, $active_graph));
				my %vars;
				foreach my $iter (@iters) {
					$vars{$_}++ for (@{ $iter->variables });
				}
				return Attean::IteratorSequence->new( iterators => \@iters, item_type => 'Attean::API::Result', variables => [keys %vars] );
			}
			die "Unimplemented path type: $path";
		} elsif ($algebra->isa('Attean::Algebra::Project')) {
			my $iter	= $self->evaluate( $child, $active_graph );
			my @vars	= map { $_->value } @{ $algebra->variables };
			return $iter->map(sub {
				my $r	= shift;
				my $b	= { map { my $t	= $r->value($_); $t	? ($_ => $t) : () } @vars };
				return Attean::Result->new( bindings => $b );
			}); #->debug('Project result');
		} elsif ($algebra->isa('Attean::Algebra::Slice')) {
			my $iter	= $self->evaluate( $child, $active_graph );
			$iter		= $iter->offset($algebra->offset) if ($algebra->offset > 0);
			$iter		= $iter->limit($algebra->limit) if ($algebra->limit >= 0);
			return $iter;
		} elsif ($algebra->isa('Attean::Algebra::Union')) {
			return Attean::IteratorSequence->new( iterators => [map { $self->evaluate($_, $active_graph) } @children], item_type => 'Attean::API::Result', variables => [$algebra->in_scope_variables] );
		} elsif ($algebra->isa('Attean::Algebra::Ask')) {
			my $iter	= $self->evaluate($child, $active_graph);
			my $result	= $iter->next;
			return Attean::ListIterator->new(values => [$result ? Attean::Literal->true : Attean::Literal->false], item_type => 'Attean::API::Term');
		} elsif ($algebra->isa('Attean::Algebra::Construct')) {
			my $iter		= $self->evaluate($child, $active_graph);
			my $patterns	= $algebra->triples;
			my %seen;
			return Attean::CodeIterator->new(
				generator => sub {
					my $r	= $iter->next;
					return unless ($r);
					my %mapping	= map { my $t = $r->value($_); $t ? ("?$_" => $t) : (); } ($r->variables);
					my $mapper	= Attean::TermMap->rewrite_map(\%mapping);
					my @triples;
					PATTERN: foreach my $p (@$patterns) {
						my @terms	= $p->apply_map($mapper)->values;
						next PATTERN unless all { $_->does('Attean::API::Term') } @terms;
						push(@triples, Attean::Triple->new(@terms));
					}
					return @triples;
				},
				item_type => 'Attean::API::Triple'
			)->grep(sub { return not($seen{shift->as_string}++); });
		} elsif ($algebra->isa('Attean::Algebra::Table')) {
			my $vars	= [map { $_->value } @{ $algebra->variables }];
			return Attean::ListIterator->new(variables => $vars, values => $algebra->rows, item_type => 'Attean::API::Result');
		}
		die "Unimplemented algebra evaluation for: $algebra";
	}

	sub _ALP {
		my $self	= shift;
		my $graph	= shift;
		my $term	= shift;
		my $path	= shift;
		my $v		= shift;
		return if (exists $v->{ $term->as_string });
		$v->{ $term->as_string }	= $term;
		
		my $iter	= $self->_eval($graph, $term, $path);
		while (my $n = $iter->next) {
			$self->_ALP($graph, $n, $path, $v);
		}
	}
	
	sub _eval {
		my $self	= shift;
		my $graph	= shift;
		my $term	= shift;
		my $path	= shift;
		my $pp		= Attean::Algebra::Path->new( subject => $term, path => $path, object => variable('o') );
		my $iter	= $self->evaluate($pp, $graph);
		my $terms	= $iter->map(sub { shift->value('o') }, 'Attean::API::Term');
		my %seen;
		return $terms->grep(sub { not $seen{ shift->as_string }++ });
	}
	
	sub _zeroLengthPath {
		my $self	= shift;
		my $s		= shift;
		my $o		= shift;
		my $graph	= shift;
		my $s_term	= ($s->does('Attean::API::Term'));
		my $o_term	= ($o->does('Attean::API::Term'));
		if ($s_term and $o_term) {
			my @r;
			push(@r, Attean::Result->new()) if ($s->equals($o));
			return Attean::ListIterator->new(variables => [], values => \@r, item_type => 'Attean::API::Result');
		} elsif ($s_term) {
			my $name	= $o->value;
			my $r		= Attean::Result->new( bindings => { $name => $s } );
			return Attean::ListIterator->new(variables => [$name], values => [$r], item_type => 'Attean::API::Result');
		} elsif ($o_term) {
			my $name	= $s->value;
			my $r		= Attean::Result->new( bindings => { $name => $o } );
			return Attean::ListIterator->new(variables => [$name], values => [$r], item_type => 'Attean::API::Result');
		} else {
			my @vars	= map { $_->value } ($s, $o);
			my $nodes	= $self->model->graph_nodes( $graph );
			return $nodes->map(
				sub {
					my $term	= shift;
					Attean::Result->new( bindings => { map { $_ => $term } @vars } );
				},
				'Attean::API::Result',
				variables => \@vars
			);
		}
	}
}

package Attean::SimpleQueryEvaluator::ExpressionEvaluator 0.029 {
	use Moo;
	use Attean::RDF;
	use Scalar::Util qw(blessed);
	use Types::Standard qw(InstanceOf);
	use URI::Escape qw(uri_escape_utf8);
	use Encode qw(encode);
	use POSIX qw(ceil floor);
	use Digest;
	use UUID::Tiny ':std';
	use List::MoreUtils qw(zip);
	use DateTime::Format::W3CDTF;
	use I18N::LangTags;
	use namespace::clean;
	
	has 'evaluator'	=> (is => 'ro', isa => InstanceOf['Attean::SimpleQueryEvaluator']);
	sub evaluate_expression {
		my $self			= shift;
		my $expr			= shift;
		my $row				= shift;
		my $active_graph	= shift;
		my $row_cache		= shift || {};
		my $impl			= $self->impl($expr, $active_graph);
		return eval { $impl->($row, row_cache => $row_cache) };
	}
	
	sub impl {
		my $self			= shift;
		my $expr			= shift;
		my $active_graph	= shift;
		my $op		= $expr->operator;
		my $true	= Attean::Literal->true;
		my $false	= Attean::Literal->false;
		if ($expr->isa('Attean::ExistsExpression')) {
			my $pattern		= $expr->pattern;
			return sub {
				my $r			= shift;
				my $table		= Attean::Algebra::Table->new( variables => [map { variable($_) } $r->variables], rows => [$r] );
				my $join		= Attean::Algebra::Join->new( children => [$table, $pattern] );
				# TODO: substitute variables at top-level of EXISTS pattern
				my $iter	= $self->evaluator->evaluate($join, $active_graph);
				return ($iter->next) ? $true : $false;
			};
		} elsif ($expr->isa('Attean::ValueExpression')) {
			my $node	= $expr->value;
			if ($node->does('Attean::API::Variable')) {
				return sub { return shift->value($node->value); };
			} else {
				return sub { return $node };
			}
		} elsif ($expr->isa('Attean::UnaryExpression')) {
			my ($child)	= @{ $expr->children };
			my $impl	= $self->impl($child, $active_graph);
			if ($op eq '!') {
				return sub {
					my $term	= $impl->(@_);
					return ($term->ebv) ? $false : $true;
				}
			} elsif ($op eq '-' or $op eq '+') {
				return sub {
					my $term	= $impl->(@_);
					die "TypeError $op" unless (blessed($term) and $term->does('Attean::API::NumericLiteral'));
					my $v	= $term->numeric_value;
					return Attean::Literal->new( value => eval "$op$v", datatype => $term->datatype );
				};
			}
			die "Unimplemented UnaryExpression evaluation: " . $expr->operator;
		} elsif ($expr->isa('Attean::BinaryExpression')) {
			my ($lhs, $rhs)	= @{ $expr->children };
			my ($lhsi, $rhsi)	= map { $self->impl($_, $active_graph) } ($lhs, $rhs);
			if ($op eq '&&') {
				return sub {
					my ($r, %args)	= @_;
					my $lbv	= eval { $lhsi->($r, %args) };
					my $rbv	= eval { $rhsi->($r, %args) };
					die "TypeError $op" unless ($lbv or $rbv);
					return $false if (not($lbv) and not($rbv->ebv));
					return $false if (not($rbv) and not($lbv->ebv));
					die "TypeError $op" unless ($lbv and $rbv);
					return ($lbv->ebv && $rbv->ebv) ? $true : $false;
				}
			} elsif ($op eq '||') {
				return sub {
					my ($r, %args)	= @_;
					my $lbv	= eval { $lhsi->($r, %args) };
					return $true if ($lbv and $lbv->ebv);
					my $rbv	= eval { $rhsi->($r, %args) };
					die "TypeError $op" unless ($rbv);
					return $true if ($rbv->ebv);
					return $false if ($lbv);
					die "TypeError $op";
				}
			} elsif ($op =~ m#^(?:[-+*/])$#) { # numeric operators: - + * /
				return sub {
					my ($r, %args)	= @_;
					($lhs, $rhs)	= map { $_->($r, %args) } ($lhsi, $rhsi);
					for ($lhs, $rhs) { die "TypeError $op" unless (blessed($_) and $_->does('Attean::API::NumericLiteral')); }
					my $lv	= $lhs->numeric_value;
					my $rv	= $rhs->numeric_value;
					return Attean::Literal->new( value => eval "$lv $op $rv", datatype => $lhs->binary_promotion_type($rhs, $op) );
				};
			} elsif ($op =~ /^!?=$/) {
				return sub {
					my ($r, %args)	= @_;
					($lhs, $rhs)	= map { $_->($r, %args) } ($lhsi, $rhsi);
					for ($lhs, $rhs) { die "TypeError $op" unless (blessed($_) and $_->does('Attean::API::Term')); }
					my $ok	= ($lhs->equals($rhs));
					$ok		= not($ok) if ($op eq '!=');
					return $ok ? $true : $false;
				}
			} elsif ($op =~ /^[<>]=?$/) {
				return sub {
					my ($r, %args)	= @_;
					($lhs, $rhs)	= map { $_->($r, %args) } ($lhsi, $rhsi);
					for ($lhs, $rhs) { die "TypeError $op" unless $_->does('Attean::API::Term'); }
					my $c	= ($lhs->compare($rhs));
					return $true if (($c < 0 and ($op =~ /<=?/)) or ($c > 0 and ($op =~ />=?/)) or ($c == 0 and ($op =~ /=/)));
					return $false;
				}
			}
			die "Unexpected operator evaluation: $op";
		} elsif ($expr->isa('Attean::FunctionExpression')) {
			my $func		= $expr->operator;
			my @children	= map { $self->impl($_, $active_graph) } @{ $expr->children };
			my %type_roles		= qw(URI IRI IRI IRI BLANK Blank LITERAL Literal NUMERIC NumericLiteral);
			my %type_classes	= qw(URI Attean::IRI IRI Attean::IRI STR Attean::Literal);
			return sub {
				my ($r, %args)	= @_;
				my $row_cache	= $args{row_cache} || {};
			
				if ($func eq 'IF') {
					my $term	= $children[0]->( $r, %args );
					return ($term->ebv) ? $children[1]->( $r, %args ) : $children[2]->( $r, %args );
				} elsif ($func eq 'IN' or $func eq 'NOTIN') {
					($true, $false)	= ($false, $true) if ($func eq 'NOTIN');
					my $child	= shift(@children);
					my $term	= $child->( $r, %args );
					foreach my $c (@children) {
						if (my $value = eval { $c->( $r, %args ) }) {
							return $true if ($term->equals($value));
						}
					}
					return $false;
				} elsif ($func eq 'COALESCE') {
					foreach my $c (@children) {
						my $t	= eval { $c->( $r, %args ) };
						next if ($@);
						return $t if $t;
					}
					return;
				}
			
				my @operands	= map { $_->( $r, %args ) } @children;
				if ($func =~ /^(STR)$/) {
					return $type_classes{$1}->new($operands[0]->value);
				} elsif ($func =~ /^([UI]RI)$/) {
					my @base	= $expr->has_base ? (base => $expr->base) : ();
					return $type_classes{$1}->new(value => $operands[0]->value, @base);
				} elsif ($func eq 'BNODE') {
					if (scalar(@operands)) {
						my $name	= $operands[0]->value;
						if (my $b = $row_cache->{bnodes}{$name}) {
							return $b;
						} else {
							my $b	= Attean::Blank->new();
							$row_cache->{bnodes}{$name}	= $b;
							return $b;
						}
					}
					return Attean::Blank->new();
				} elsif ($func eq 'LANG') {
					die "TypeError: LANG" unless ($operands[0]->does('Attean::API::Literal'));
					return Attean::Literal->new($operands[0]->language // '');
				} elsif ($func eq 'LANGMATCHES') {
					my ($lang, $match)	= map { $_->value } @operands;
					if ($match eq '*') {
						# """A language-range of "*" matches any non-empty language-tag string."""
						return ($lang ? $true : $false);
					} else {
						return (I18N::LangTags::is_dialect_of( $lang, $match )) ? $true : $false;
					}
				} elsif ($func eq 'DATATYPE') {
					return $operands[0]->datatype;
				} elsif ($func eq 'BOUND') {
					return $operands[0] ? $true : $false;
				} elsif ($func eq 'RAND') {
					return Attean::Literal->new( value => rand(), datatype => 'http://www.w3.org/2001/XMLSchema#double' );
				} elsif ($func eq 'ABS') {
					return Attean::Literal->new( value => abs($operands[0]->value), $operands[0]->construct_args );
				} elsif ($func =~ /^(?:CEIL|FLOOR)$/) {
					my $v	= $operands[0]->value;
					return Attean::Literal->new( value => (($func eq 'CEIL') ? ceil($v) : floor($v)), $operands[0]->construct_args );
				} elsif ($func eq 'ROUND') {
					return Attean::Literal->new( value => sprintf('%.0f', (0.000000000000001 + $operands[0]->numeric_value)), $operands[0]->construct_args );
				} elsif ($func eq 'CONCAT') {
					my $all_lang	= 1;
					my $all_str		= 1;
					my $lang;
					foreach my $n (@operands) {
						die "CONCAT called with a non-literal argument" unless ($n->does('Attean::API::Literal'));
						if ($n->datatype->value ne 'http://www.w3.org/2001/XMLSchema#string') {
							die "CONCAT called with a datatyped-literal other than xsd:string";
						} elsif ($n->language) {
							$all_str	= 0;
							if (defined($lang) and $lang ne $n->language) {
								$all_lang	= 0;
							} else {
								$lang	= $n->language;
							}
						} else {
							$all_lang	= 0;
							$all_str	= 0;
						}
					}
					my %strtype;
					if ($all_lang and $lang) {
						$strtype{language}	= $lang;
					} elsif ($all_str) {
						$strtype{datatype}	= 'http://www.w3.org/2001/XMLSchema#string'
					}
					return Attean::Literal->new( value => join('', map { $_->value } @operands), %strtype );
				} elsif ($func eq 'SUBSTR') {
					my $str		= shift(@operands);
					my @args	= map { $_->numeric_value } @operands;
					my $v		= scalar(@args == 1) ? substr($str->value, $args[0]-1) : substr($str->value, $args[0]-1, $args[1]);
					return Attean::Literal->new( value => $v, $str->construct_args );
				} elsif ($func eq 'STRLEN') {
					return Attean::Literal->integer(length($operands[0]->value));
				} elsif ($func eq 'REPLACE') {
					my ($node, $pat, $rep)	= @operands;
					die "TypeError: REPLACE called without a literal arg1 term" unless (blessed($node) and $node->does('Attean::API::Literal'));
					die "TypeError: REPLACE called without a literal arg2 term" unless (blessed($pat) and $pat->does('Attean::API::Literal'));
					die "TypeError: REPLACE called without a literal arg3 term" unless (blessed($rep) and $rep->does('Attean::API::Literal'));
					die "TypeError: REPLACE called with a datatyped (non-xsd:string) literal" if ($node->datatype and $node->datatype->value ne 'http://www.w3.org/2001/XMLSchema#string');
					my ($value, $pattern, $replace)	= map { $_->value } @operands;
					die "EvaluationError: REPLACE called with unsafe ?{} match pattern" if (index($pattern, '(?{') != -1 or index($pattern, '(??{') != -1);
					die "EvaluationError: REPLACE called with unsafe ?{} replace pattern" if (index($replace, '(?{') != -1 or index($replace, '(??{') != -1);
	
					$replace	=~ s/\\/\\\\/g;
					$replace	=~ s/\$(\d+)/\$$1/g;
					$replace	=~ s/"/\\"/g;
					$replace	= qq["$replace"];
					no warnings 'uninitialized';
					$value	=~ s/$pattern/"$replace"/eeg;
					return Attean::Literal->new(value => $value, $node->construct_args);
				} elsif ($func =~ /^[UL]CASE$/) {
					return Attean::Literal->new( value => ($func eq 'UCASE' ? uc($operands[0]->value) : lc($operands[0]->value) ), $operands[0]->construct_args );
				} elsif ($func eq 'ENCODE_FOR_URI') {
					return Attean::Literal->new( uri_escape_utf8($operands[0]->value) );
				} elsif ($func eq 'CONTAINS') {
					my ($node, $pat)	= @operands;
					my ($lit, $plit)	= map { $_->value } @operands;
					die "TypeError: CONTAINS" if ($node->language and $pat->language and $node->language ne $pat->language);
					return (index($lit, $plit) >= 0) ? $true : $false;
				} elsif ($func eq 'STRSTARTS' or $func eq 'STRENDS') {
					my ($lit, $plit)	= map { $_->value } @operands;
					if ($func eq 'STRENDS') {
						my $pos		= length($lit) - length($plit);
						return (rindex($lit, $plit) == $pos) ? $true : $false;
					} else {
						return (index($lit, $plit) == 0) ? $true : $false;
					}
				} elsif ($func eq 'STRBEFORE' or $func eq 'STRAFTER') {
					my ($node, $substr)	= @operands;

					die "$func called without a literal arg1 term" unless (blessed($node) and $node->does('Attean::API::Literal'));
					die "$func called without a literal arg2 term" unless (blessed($substr) and $substr->does('Attean::API::Literal'));
					die "$func called with a datatyped (non-xsd:string) literal" if ($node->datatype and $node->datatype->value ne 'http://www.w3.org/2001/XMLSchema#string');

					my $lhs_simple	= (not($node->language) and ($node->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string'));
					my $rhs_simple	= (not($substr->language) and ($substr->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string'));
					if ($lhs_simple and $rhs_simple) {
						# ok
					} elsif ($node->language and $substr->language and $node->language eq $substr->language) {
						# ok
					} elsif ($node->language and $rhs_simple) {
						# ok
					} else {
						die "$func called with literals that are not argument compatible";
					}
	
					my $value	= $node->value;
					my $match	= $substr->value;
					my $i		= index($value, $match, 0);
					if ($i < 0) {
						return Attean::Literal->new('');
					} else {
						if ($func eq 'STRBEFORE') {
							return Attean::Literal->new(value => substr($value, 0, $i), $node->construct_args);
						} else {
							return Attean::Literal->new(value => substr($value, $i+length($match)), $node->construct_args);
						}
					}
				} elsif ($func =~ /^(?:YEAR|MONTH|DAY|HOURS|MINUTES)$/) {
					my $method	= lc($func =~ s/^(HOUR|MINUTE)S$/$1/r);
					my $dt		= $operands[0]->datetime;
					return Attean::Literal->integer($dt->$method());
				} elsif ($func eq 'SECONDS') {
					my $dt	= $operands[0]->datetime;
					return Attean::Literal->decimal($dt->second());
				} elsif ($func eq 'TZ' or $func eq 'TIMEZONE') {
					my $dt	= $operands[0]->datetime;
					my $tz	= $dt->time_zone;
					if ($tz->is_floating) {
						return Attean::Literal->new('') if ($func eq 'TZ');
						die "TIMEZONE called with a dateTime without a timezone";
					}
					return Attean::Literal->new('Z') if ($func eq 'TZ' and $tz->is_utc);
					if ($tz) {
						my $offset	= $tz->offset_for_datetime( $dt );
						my $hours	= 0;
						my $minutes	= 0;
						my $minus	= ($func eq 'TZ') ? '+' : '';
						if ($offset < 0) {
							$minus	= '-';
							$offset	= -$offset;
						}

						my $duration	= "${minus}PT";
						if ($offset >= 60*60) {
							my $h	= int($offset / (60*60));
							$duration	.= "${h}H" if ($h > 0);
							$hours	= int($offset / (60*60));
							$offset	= $offset % (60*60);
						}
						if ($offset >= 60) {
							my $m	= int($offset / 60);
							$duration	.= "${m}M" if ($m > 0);
							$minutes	= int($offset / 60);
							$offset	= $offset % 60;
						}
						my $seconds	= int($offset);
						my $s		= int($offset);
						$duration	.= "${s}S" if ($s > 0 or $duration eq 'PT');
			
						return ($func eq 'TZ')
							? Attean::Literal->new(sprintf('%s%02d:%02d', $minus, $hours, $minutes))
							: Attean::Literal->new( value => $duration, datatype => "http://www.w3.org/2001/XMLSchema#dayTimeDuration");
					} else {
						return Attean::Literal->new('') if ($func eq 'TZ');
						die "TIMEZONE called without a valid dateTime";
					}
				} elsif ($func eq 'NOW') {
					my $value	= DateTime::Format::W3CDTF->new->format_datetime( DateTime->now );
					return Attean::Literal->new( value => $value, datatype => 'http://www.w3.org/2001/XMLSchema#dateTime' );
				} elsif ($func =~ /^(?:STR)?UUID$/) {
					return Attean::Literal->new(uc(uuid_to_string(create_uuid()))) if ($func eq 'STRUUID');
					return Attean::IRI->new('urn:uuid:' . uc(uuid_to_string(create_uuid())));
				} elsif ($func =~ /^(MD5|SHA1|SHA256|SHA384|SHA512)$/) {
					my $hash	= $func =~ s/SHA/SHA-/r;
					my $digest	= eval { Digest->new($hash)->add(encode('UTF-8', $operands[0]->value, Encode::FB_CROAK))->hexdigest };
					return Attean::Literal->new($digest);
				} elsif ($func eq 'STRLANG') {
					my ($str, $lang)	= @operands;
					my @values	= map { $_->value } @operands;
					die "TypeError: STRLANG must be called with two plain literals" unless (blessed($str) and $str->does('Attean::API::Literal') and blessed($lang) and $lang->does('Attean::API::Literal'));
					die "TypeError: STRLANG not called with a simple literal" unless ($str->datatype->value eq 'http://www.w3.org/2001/XMLSchema#string' and not($str->language));
					return Attean::Literal->new( value => $values[0], language => $values[1] );
				} elsif ($func eq 'STRDT') {
					die "TypeError: STRDT" unless ($operands[0]->does('Attean::API::Literal') and not($operands[0]->language));
					if (my $dt = $operands[0]->datatype) {
						die "TypeError: STRDT" unless ($dt->value eq 'http://www.w3.org/2001/XMLSchema#string');
					}
					die "TypeError: STRDT" unless ($operands[1]->does('Attean::API::IRI'));
					my @values	= map { $_->value } @operands;
					return Attean::Literal->new( value => $values[0], datatype => $values[1] );
				} elsif ($func eq 'SAMETERM') {
					my ($a, $b)	= @operands;
					die "TypeError: SAMETERM" unless (blessed($operands[0]) and blessed($operands[1]));
					return $false if ($a->compare($b));
					return ($a->value eq $b->value) ? $true : $false;
				} elsif ($func =~ /^IS([UI]RI|BLANK|LITERAL|NUMERIC)$/) {
					return $operands[0]->does("Attean::API::$type_roles{$1}") ? $true : $false;
				} elsif ($func eq 'REGEX') {
					my ($value, $pattern)	= map { $_->value } @operands;
					return ($value =~ /$pattern/) ? $true : $false;
				}
				die "Unimplemented FunctionExpression evaluation: " . $expr->operator;
			};
		} elsif ($expr->isa('Attean::AggregateExpression')) {
			my $agg		= $expr->operator;
			my ($child)	= @{ $expr->children };
			if ($agg eq 'COUNT') {
				if ($child) {
					my $impl	= $self->impl($child, $active_graph);
					return sub {
						my ($rows, %args)	= @_;
						my @terms	= grep { blessed($_) } map { $impl->($_, %args) } @{ $rows };
						if ($expr->distinct) {
							my %seen;
							@terms	= grep { not($seen{$_->as_string}++) } @terms;
						}
						return Attean::Literal->integer(scalar(@terms));
					};
				} else {
					return sub {
						my ($rows, %args)	= @_;
						return Attean::Literal->integer(scalar(@$rows));
					};
				}
			} elsif ($agg =~ /^(?:SAMPLE|MIN|MAX|SUM|AVG|GROUP_CONCAT)$/) {
				my $impl	= $self->impl($child, $active_graph);
				if ($agg eq 'SAMPLE') {
					return sub {
						my ($rows, %args)	= @_;
						return $impl->( shift(@$rows), %args )
					};
				} elsif ($agg eq 'MIN' or $agg eq 'MAX') {
					my $expect	= ($agg eq 'MIN') ? 1 : -1;
					return sub {
						my ($rows, %args)	= @_;
						my $extrema;
						foreach my $r (@$rows) {
							my $t	= $impl->( $r, %args );
							return if (not($t) and $agg eq 'MIN');	# unbound is always minimal
							next if (not($t));						# unbound need not be considered for MAX
							$extrema	= $t if (not($extrema) or $extrema->compare($t) == $expect);
						}
						return $extrema;
					};
				} elsif ($agg eq 'SUM' or $agg eq 'AVG') {
					return sub {
						my ($rows, %args)	= @_;
						my $count	= 0;
						my $sum		= Attean::Literal->integer(0);
						my %seen;
						foreach my $r (@$rows) {
							my $term	= $impl->( $r, %args );
							if ($expr->distinct) {
								next if ($seen{ $term->as_string }++);
							}
							if ($term->does('Attean::API::NumericLiteral')) {
								$count++;
								$sum	= Attean::Literal->new( value => ($sum->numeric_value + $term->numeric_value), datatype => $sum->binary_promotion_type($term, '+') );
							} else {
								die "TypeError: AVG";
							}
						}
						if ($agg eq 'AVG') {
							$sum	= not($count) ? undef : Attean::Literal->new( value => ($sum->numeric_value / $count), datatype => $sum->binary_promotion_type(Attean::Literal->integer($count), '/') );
						}
						return $sum;
					};
				} elsif ($agg eq 'GROUP_CONCAT') {
					my $sep	= $expr->scalar_vars->{ 'seperator' } // ' ';
					return sub {
						my ($rows, %args)	= @_;
						my %seen;
						my @strings;
						foreach my $r (@$rows) {
							my $term	= eval { $impl->( $r, %args ) };
							if ($expr->distinct) {
								next if ($seen{ blessed($term) ? $term->as_string : '' }++);
							}
							push(@strings, $term->value // '');
						}
						return Attean::Literal->new(join($sep, sort @strings));
					};
				}
			}
			die "Unimplemented AggregateExpression evaluation: " . $expr->operator;
		} elsif ($expr->isa('Attean::CastExpression')) {
			my ($child)	= @{ $expr->children };
			my $impl	= $self->impl( $child, $active_graph );
			my $type	= $expr->datatype;
			return sub {
				my ($r, %args)	= @_;
				my $term	= $impl->($r, %args);
				# TODO: reformat syntax for xsd:double
				my $cast	= Attean::Literal->new( value => $term->value, datatype => $type );
				return $cast->canonicalized_term if ($cast->does('Attean::API::CanonicalizingLiteral'));
				return $cast;
			}
		} else {
			Carp::confess "No impl for expression " . $expr->as_string;
		}
	}
}

1;

__END__

=back

=head1 BUGS

Please report any bugs or feature requests to through the GitHub web interface
at L<https://github.com/kasei/attean/issues>.

=head1 SEE ALSO



=head1 AUTHOR

Gregory Todd Williams  C<< <gwilliams@cpan.org> >>

=head1 COPYRIGHT

Copyright (c) 2014--2020 Gregory Todd Williams.
This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut
