use v5.14;
use warnings;

=head1 NAME

Attean::API::Iterator - Typed iterator

=head1 VERSION

This document describes Attean::API::Iterator version 0.037

=head1 DESCRIPTION

The Attean::API::Iterator role defines a common API for typed iterators.
This package also defines several type-specific iterator roles:

=over 4

=item * L<Attean::API::TripleIterator>

=item * L<Attean::API::QuadIterator>

=item * L<Attean::API::MixedStatementIterator>

=item * L<Attean::API::ResultIterator>

=back

These roles will automatically be applied to iterators during construction when appropriate.

=head1 ATTRIBUTES

The following attributes exist:

=over 4

=item C<< item_type >>

A string indicating the type of elements returned by the iterator.

=back

=head1 REQUIRED METHODS

The following methods are required by the L<Attean::API::Iterator> role:

=over 4

=item C<< next >>

Returns the next element from the iterator, or C<< undef >> upon exhaustion.

=back

=head1 METHODS

The L<Attean::API::Iterator> role provides default implementations of the
following methods:

=over 4

=item C<< elements >>

Returns a list of all remaining elements in the iterator.

=item C<< map( \&mapper [, $result_type] ) >>

Returns a new L<Attean::API::Iterator> object with each element mapped using
the supplied C<< &mapper >> function. If the iterator elements are of the same
type as those in the referent iterator, only a mapping function is required.
Otherwise, the supplied L<Type::Tiny> C<< $result_type >> object must indicate
the new iterator's type information.

=item C<< grep( \&filter ) >>

Returns a new L<Attean::API::Iterator> object that filters elements from the
referent iterator based on whether calling C<< &filter( $element ) >> for each
C<< $element >> results in a true value.

=item C<< offset( $offset ) >>

Returns the L<Attean::API::Iterator> referent after skipping the first
C<< $offset >> elements.

=item C<< limit( $limit ) >>

Returns a new L<Attean::API::Iterator> object which returns the first
C<< $limit >> elements of the referent.

=item C<< materialize >>

Returns a new L<Attean::API::RepeatableIterator> object containing all the
elements from the referent.

=cut

package Attean::API::Iterator 0.037 {
	use Scalar::Util qw(blessed);
	use Types::Standard qw(Str Object InstanceOf);
	use Carp qw(confess);

	use Moo::Role;
	
	has 'item_type' => (is => 'ro', isa => Str, required => 1);
	requires 'next';

	sub BUILD {}
	around 'BUILD' => sub {
		my $orig	= shift;
		my $self	= shift;
		my $args	= shift;
		$self->$orig($args);
		my $role	= $self->item_type;
		if (Moo::Role->is_role($role)) {
			my $check	= sub {
				my $check = shift;
				return ($role eq $check or Moo::Role::does_role($role, $check));
			};
			if ($check->('Attean::API::Quad')) {
				Moo::Role->apply_roles_to_object($self, 'Attean::API::QuadIterator');
			} elsif ($check->('Attean::API::Triple')) {
				Moo::Role->apply_roles_to_object($self, 'Attean::API::TripleIterator');
			} elsif ($check->('Attean::API::TripleOrQuad')) {
				Moo::Role->apply_roles_to_object($self, 'Attean::API::MixedStatementIterator');
			} elsif ($check->('Attean::API::Result')) {
				Moo::Role->apply_roles_to_object($self, 'Attean::API::ResultIterator');
				my $vars	= $args->{variables} // confess "Construction of a Attean::API::ResultIterator must include a variables list";
				$self->variables($vars);
			} elsif ($check->('Attean::API::Term')) {
				Moo::Role->apply_roles_to_object($self, 'Attean::API::TermIterator');
			} elsif ($check->('Attean::API::ResultOrTerm')) {
				Moo::Role->apply_roles_to_object($self, 'Attean::API::ResultOrTermIterator');
				$self->variables($args->{variables} || []);
			}

			if ($self->does('Attean::API::RepeatableIterator') and $check->('Attean::API::Binding')) {
				Moo::Role->apply_roles_to_object($self, 'Attean::API::CanonicalizingBindingSet');
			}
		}
	};
	
	if ($ENV{ATTEAN_TYPECHECK}) {
		around 'next' => sub {
			my $orig	= shift;
			my $self	= shift;
			my $type	= $self->item_type;
			my $class	= ref($self);
			my $term	= $self->$orig(@_);
			return unless defined($term);
			if (blessed($term)) {
				unless ($term->does($type) or $term->isa($type)) {
					die "${class} returned an element that failed conformance check for $type: $term";
				}
			}
			return $term;
		};
	}
	
	sub elements {
		my $self	= shift;
		my @elements;
		while (my $item = $self->next) { push(@elements, $item); }
		return @elements;
	}
	
	sub map {
		my $self	= shift;
		my $block	= shift;
		my $type	= shift || $self->item_type;
		
		my $generator;
		if (blessed($block) and $block->does('Attean::Mapper')) {
			$generator	= sub {
				my $item	= $self->next();
				return unless defined($item);
				my $new		= $block->map($item);
				return $new;
			}
		} else {
			my @buffer;
			$generator	= sub {
				while (1) {
					return shift(@buffer) if (scalar(@buffer));
					my $item	= $self->next();
					return unless defined($item);
					local($_)	= $item;
					push(@buffer, $block->($item));
				}
			}
		}
		
		# copy variables into new iterator if $self does ::ResultIterator or ::ResultOrTermIterator
		my %args	= @_;
		if ($self->can('variables') and not exists $args{variables}) {
			$args{variables}	= $self->variables;
		}
		
		return Attean::CodeIterator->new( %args, item_type => $type, generator => $generator );
	}

	sub grep {
		my $self	= shift;
		my $block	= shift;
		
		# copy variables into new iterator if $self does ::ResultIterator or ::ResultOrTermIterator
		my %args	= @_;
		if ($self->can('variables') and not exists $args{variables}) {
			$args{variables}	= $self->variables;
		}
		
		Attean::CodeIterator->new(
			%args,
			item_type => $self->item_type,
			generator => sub {
				while (1) {
					my $item	= $self->next();
					return unless defined($item);
					local($_)	= $item;
					return $item if ($block->($item));
				}
			}
		);
	}
	
	sub offset {
		my $self	= shift;
		my $offset	= shift;
		$self->next for (1 .. $offset);
		return $self;
	}
	
	sub limit {
		my $self	= shift;
		my $limit	= shift;
		
		# copy variables into new iterator if $self does ::ResultIterator or ::ResultOrTermIterator
		my %args	= @_;
		if ($self->can('variables') and not exists $args{variables}) {
			$args{variables}	= $self->variables;
		}
		
		Attean::CodeIterator->new(
			%args,
			item_type => $self->item_type,
			generator => sub {
				return unless $limit;
				my $item	= $self->next();
				return unless defined($item);
				$limit--;
				return $item;
			}
		);
	}
	
	sub materialize {
		my $self	= shift;
		my @data	= $self->elements;
		my %args	= @_;
		if ($self->can('variables') and not exists $args{variables}) {
			$args{variables}	= $self->variables;
		}
		
		return Attean::ListIterator->new( %args, values => \@data, item_type => $self->item_type );
	}
	
=item C<< debug( [$name] ) >>

Print each item as it is consumed (with the string generated by C<< as_string >>), prepended by C<< $name >>.

=cut

	sub debug {
		my $self	= shift;
		my $name	= shift // 'Iterator item';
		return $self->grep(sub { my $r = shift; say "$name: " . $r->as_string; return 1; });
	}
}

package Attean::API::StringyItemIterator 0.037 {
	use Moo::Role;
	sub uniq {
		my $self	= shift;
		
		my %seen;
		return $self->grep(sub {
			my $r	= shift;
			return not($seen{ $r->as_string }++);
		});
	}
}

package Attean::API::RepeatableIterator 0.037 {
	use Moo::Role;
	requires 'reset';
	
	sub elements {
		my $self	= shift;
		my @elements;
		while (my $item = $self->next) { push(@elements, $item); }
		$self->reset;
		return @elements;
	}
	
	sub peek {
		my $self	= shift;
		my $item	= $self->next;
		$self->reset;
		return $item;
	}

	sub materialize {
		my $self	= shift;
		return $self;
	}

	sub size {
		my $self	= shift;
		my @elements	= $self->elements;
		return scalar(@elements);
	}
	
	with 'Attean::API::Iterator';
}

package Attean::API::CanonicalizingBindingIterator {
	use Moo::Role;
	sub canonicalize {
		my $self	= shift;
		my $mapper	= Attean::TermMap->canonicalization_map;
		return $self->map(sub { shift->apply_map( $mapper ) });
	}
}

package Attean::API::ResultOrTermIterator 0.037 {
	use Moo::Role;
	use Types::Standard qw(ArrayRef Str);
	has 'variables' => (is => 'rw', isa => ArrayRef[Str], default => sub { [] });
	with 'Attean::API::StringyItemIterator';
	
	sub canonicalize {
		my $self	= shift;
		my $mapper	= Attean::TermMap->canonicalization_map;
		return $self->map(sub{
			my $item	= shift;
			if ($item->does('Attean::API::Term')) {
				return $mapper->map($item);
			} else {
				my %values	= map { $_ => $mapper->map($item->value($_)) } $item->variables;
				return Attean::Result->new( bindings => \%values );
			}
		});
	}
	
	around 'grep' => sub {
		my $orig	= shift;
		my $self	= shift;
		my $block	= shift;
		my $iter	= $orig->($self, $block, @_);
		Attean::CodeIterator->new(
			item_type => $iter->item_type,
			generator => sub {
				return $iter->next();
			},
			variables => $self->variables,
		);
	};
}

package Attean::API::StatementIterator 0.037 {
	use Moo::Role;
	use Scalar::Util qw(blessed);
	with 'Attean::API::StringyItemIterator';
	requires 'variables';

	sub matching_pattern {
		my $self	= shift;
		my @nodes	= @_;
		
		if (scalar(@nodes) == 1 and $nodes[0]->does('Attean::API::QuadPattern')) {
			my $pattern	= $nodes[0];
			@nodes	= $pattern->values;
		}

		my %bound;
		my @pos_names	= $self->variables;
		foreach my $pos (0 .. $#pos_names) {
			my $n	= $nodes[ $pos ];
			if (blessed($n)) {
				$bound{ $pos_names[$pos] }	= $n;
			}
		}
		
		my $pattern	= Attean::QuadPattern->new( %bound );
		return $self->grep(sub {
			my $q	= shift;
			my $binding	= $pattern->unify($q);
			return $binding ? 1 : 0;
		});
	}
}

package Attean::API::TripleIterator 0.037 {
	use Moo::Role;
	with 'Attean::API::CanonicalizingBindingIterator';
	with 'Attean::API::StatementIterator';

	sub as_quads {
		my $self	= shift;
		my $graph	= shift;
		return $self->map(sub { $_->as_quad($graph) }, 'Attean::API::Quad');
	}

	sub variables {
		return qw(subject predicate object);
	}
}

package Attean::API::QuadIterator 0.037 {
	use Moo::Role;
	with 'Attean::API::CanonicalizingBindingIterator';
	with 'Attean::API::StatementIterator';
	
	sub variables {
		return qw(subject predicate object graph);
	}
}

package Attean::API::MixedStatementIterator 0.037 {
	use Moo::Role;
	with 'Attean::API::CanonicalizingBindingIterator';
	with 'Attean::API::StringyItemIterator';
	sub as_quads {
		my $self	= shift;
		my $graph	= shift;
		return $self->map(
			sub { $_->does('Attean::API::Quad') ? $_ : $_->as_quad($graph) },
			'Attean::API::Quad'
		);
	}
}

package Attean::API::ResultIterator 0.037 {
	use Types::Standard qw(Str ArrayRef);
	use Moo::Role;
	
	with 'Attean::API::CanonicalizingBindingIterator';
	with 'Attean::API::StringyItemIterator';
	has 'variables' => (is => 'rw', isa => ArrayRef[Str], required => 1);
	sub join {
		my $self	= shift;
		my $rhs		= shift;
		my @vars	= keys %{ { map { $_ => 1 } (@{ $self->variables }, @{ $rhs->variables }) } };
		
		my @rhs		= $rhs->elements;
		my @results;
		while (my $lhs = $self->next) {
			foreach my $rhs (@rhs) {
				if (my $j = $lhs->join($rhs)) {
					push(@results, $j);
				}
			}
		}
		return Attean::ListIterator->new( values => \@results, item_type => $self->item_type, variables => \@vars);
	}

	with 'Attean::API::ResultOrTermIterator';
}

package Attean::API::TermIterator 0.037 {
	use Moo::Role;
	sub canonicalize {
		my $self	= shift;
		my $mapper	= Attean::TermMap->canonicalization_map;
		return $self->map( $mapper );
	}
	with 'Attean::API::CanonicalizingBindingIterator';
	with 'Attean::API::StringyItemIterator';
}

1;

__END__

=back

=head2 Methods on Roles Supporting Stringification

For iterators over roles that provide an C<as_string> method, extra methods
are provided. These iterators are:

Attean::API::ResultOrTermIterator
Attean::API::StatementIterator
Attean::API::MixedStatementIterator
Attean::API::ResultIterator
Attean::API::TermIterator

They provide the following methods:

=over 4

=item C<< uniq >>

Returns a new iterator providing unique results (based on the stringified value of the underlying elements).

=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

L<Attean::API::RepeatableIterator>



=head1 AUTHOR

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

=head1 COPYRIGHT

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

=cut
