package PROP::ResultSet::Link;

use strict;
use DBI;
use PROP::Constants;
use Hash::Util qw/lock_keys/;
use PROP::SQL::Select;
use Carp;

sub new {
    my ($invocant, $query) = @_;
    my $class = ref($invocant) || $invocant;
    my $self = bless({}, $class);

    $self->{-query} = $query;
    $self->{-lower_bound} = 0;
    $self->{-stmt} = undef;
    $self->{-saved_row} = undef;
    $self->{-returned_rows} = 0;
    $self->{-done} = 0;
    $self->{-sth} = undef;
    $self->{-last_instance} = undef;

    lock_keys(%$self) if DEBUG;

    $self->build_base_statement();
    $self->execute_next_statement();

    return $self;
}

sub get_link {
    my ($self) = @_;
    return $self->{-query}->get_link();
}

sub get_relationship {
    my ($self) = @_;
    return $self->{-query}->get_relationship();
}

sub get_link_table_name {
    my ($self) = @_;
    return $self->{-query}->get_link()->get_table_name();
}

sub get_parent_table_name {
    my ($self) = @_;
    return $self->{-query}->get_link()->get_parent_table_name();
}

sub get_child_table_name {
    my ($self) = @_;
    return $self->{-query}->get_link()->get_child_table_name();
}

sub put_last_result {
    my ($self, $instance) = @_;
    $self->{-last_instance} = $instance;
}

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

    my (@instances, $instance);
    push(@instances, $instance) while($instance = $self->get_next_result());
    return \@instances;
}

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

    if($self->{-last_instance}) {
	my $result = $self->{-last_instance};
	$self->{-last_instance} = undef;
	return $result;
    }

    return undef if $self->{-done};

    my $last_pk_value = 0;
    my $result = PROP::ResultSet::Link::Result->new();

    if($self->{-saved_row}) {
	my ($pk_value, $object) = $self->instantiate_object($self->{-saved_row});
	$result->set_pk_value($pk_value);
	$last_pk_value = $pk_value;
	$result->add_relative($object);
	$self->{-saved_row} = undef;
    }

    while(1) {
	my @row;

	while(@row = $self->{-sth}->fetchrow_array()) {
	    $self->{-returned_rows}++;

	    if($last_pk_value and $row[0] != $last_pk_value) {
		$self->{-saved_row} = \@row;
		last;
	    }

	    my ($pk_value, $object) = $self->instantiate_object(\@row);
	    $result->set_pk_value($pk_value);
	    $last_pk_value = $pk_value;
	    $result->add_relative($object);
	}

	last if $self->{-saved_row};

	if($self->{-returned_rows} < $self->{-query}->get_buffer_size()) {
	    $self->{-done} = 1;
	    last;
	}

	$self->execute_next_statement();
    }

    return $result->get_pk_value() ? $result : undef;
}

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

    $self->{-returned_rows} = 0;

    $self->{-stmt}->set_limit([$self->{-lower_bound}, $self->{-query}->get_buffer_size()]);

    $self->{-lower_bound} += $self->{-query}->get_buffer_size();

    $self->{-sth} = PROP::DBH->get_handle()->prepare($self->{-stmt}->stringify());

    $self->{-sth}->execute($self->{-query}->get_bindings());
}

sub instantiate_object {
    my ($self, $row) = @_;

    my ($class, $obj);
    my $r = $self->{-query}->get_relationship();
    my $pk_value = shift(@$row);

    if($r eq 'parents') {
	$class = $self->{-query}->get_link()->get_parent_class();
	$obj = $class->new();
	$obj->_set_contextual_value($self->{-query}->get_link()->get_child_field_name(), $pk_value);
    }
    else {
	$class = $self->{-query}->get_link()->get_child_class();
	$obj = $class->new();
	$obj->_set_contextual_value($self->{-query}->get_link()->get_parent_field_name(), $pk_value);
    }

    foreach ($obj->get_field_names()) {
	$obj->set_field_value($_, shift(@$row));
    }

    foreach ($self->{-query}->get_link()->get_contextual_field_names()) {
	$obj->_set_contextual_value($_, shift(@$row));
    }

    return ($pk_value, $obj);
}

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

    my $link = $self->{-query}->get_link();
    my $query = $self->{-query};

    my $stmt = new PROP::SQL::Select;
    $self->{-stmt} = $stmt;

    my %added_tables;

    $stmt->add_table($link->get_table_name() . ' l');
    $added_tables{$link->get_table_name()} = 1;

    $stmt->add_table($link->get_parent_table_name() . ' p');
    $added_tables{$link->get_parent_table_name()} = 1;

    $stmt->add_table($link->get_child_table_name() . ' c');
    $added_tables{$link->get_child_table_name()} = 1;

    $stmt->push_conditional_expression('p.' . $link->get_parent_class()->get_pk_name() . 
			  '='  .
			  'l.' . $link->get_parent_field_name());

    $stmt->push_conditional_expression('c.' . $link->get_child_class()->get_pk_name() . 
			  '='  .
			  'l.' . $link->get_child_field_name());

    $stmt->push_conditional_expression($_) foreach ($query->get_expressions());
    $stmt->push_ordering($_)  foreach ($query->get_orderings());

    my $r = $query->get_relationship();

    if($r eq 'parents') {
	$stmt->push_field('c.' . $link->get_child_class()->get_pk_name());

	foreach ($link->get_parent_class()->get_field_names()) {
	    $stmt->push_field('p.' . $_);
	}

	$stmt->unshift_ordering('c.' . $link->get_child_class()->get_pk_name());
    }
    elsif($r eq 'children') {
	$stmt->push_field('p.' . $link->get_parent_class()->get_pk_name());

	foreach ($link->get_child_class()->get_field_names()) {
	    $stmt->push_field('c.' . $_);
	}

	$stmt->unshift_ordering('p.' . $link->get_parent_class()->get_pk_name());
    }
    else {
	my $msg = "unknown relationship";
	die new PROP::Exception($msg);
    }

    $stmt->push_field('l.' . $_) foreach ($link->get_contextual_field_names());
}

package PROP::ResultSet::Link::Result;

use strict;
use PROP::Constants;
use Hash::Util qw/lock_keys/;

sub new {
    my ($invocant, $pk_value, $relatives) = @_;
    my $self = bless({}, ref($invocant) || $invocant);

    $self->{-pk_value} = $pk_value || 0;
    $self->{-relatives} = $relatives || [];

    lock_keys(%$self) if DEBUG;

    return $self;
}

sub get_pk_value {
    my ($self) = @_;
    return $self->{-pk_value};
}

sub set_pk_value {
    my ($self, $pk_value) = @_;
    $self->{-pk_value} = $pk_value;
}

sub get_relatives {
    my ($self) = @_;
    return $self->{-relatives};
}

sub add_relative {
    my ($self, $relative) = @_;
    push(@{$self->{-relatives}}, $relative);
}

1;

=head1 Name

PROP::ResultSet::Link::ResultSet

=head1 Description

Objects of this class are used to instantiate the results of a query
specified by the PROP::Query::Link object passed to its constructor.
The actual results can be obtained by repeated invocation of
get_next_result() which will return an instance of the class
PROP::ResultSet::Link::Result as long as there are more results, and
eventually undef.  Each PROP::ResultSet::Link::Result object holds the
id of an object and a list of its relatives that were loaded from the
database by the specified query.

=head1 Synopsis

 $ql  = new PROP::Query::Link(...);
 $rsl = new PROP::ResultSet::Link($ql);

 while($result = $rsl->get_next_result()) {
     # do stuff with $result, an instance of PROP::ResultSet::Link::Result
 }

=head1 Methods

=over

=item new

 $rsl = PROP::ResultSet::Link->new($ql);

This method constructs a new PROP::ResultSet::Link, as specified by the
PROP::Query::Link $ql.

=item get_next_result

 $result = $rsl->get_next_result()

This method returns the next PROP::ResultSet::Link::Result in the
result set, from which the loaded relatives for a given object can be
extracted.

=back

=head1 Name

PROP::ResultSet::Link::Result

=head1 Description

An instance of this class is returned upon each invocation of the
get_next_result() method of a PROP::ResultSet::Link object.  It
contains the results from the result set that correspond to the
relatives for one particular object.  Specifically, it contains the
primary key value of the object for which relatives were loaded, and a
list containing those relatives.

=head1 Synopsis

 $result = $rsl->get_next_result();
 print "object primary key: ", $result->get_pk_value(), "\n";
 foreach $relative (@{$result->get_relatives()}) {
     print "relative primary key: ", $relative->get_pk_value(), "\n";
 }

=head1 Methods

=over

=item get_pk_value

 $result->get_pk_value()

This method returns the value of the primary key of the object for
which a list of relatives has been returned within this
PROP::ResultSet::Link::Result object.

=item get_relatives

 $result->get_relatives()

This method returns an array reference that contains the relatives
that were loaded for a specific object as the result of the query
specified for the PROP::ResultSet::Link object that returned this
PROP::ResultSet::Link::Result object.  Each of these relatives is an
instance of a subclass of PROP::Object.

=back

=head1 Author

Andrew Gibbs (awgibbs@awgibbs.com,andrew.gibbs@nist.gov)

=head1 Legalese

This software was developed at the National Institute of Standards and
Technology by employees of the Federal Government in the course of
their official duties. Pursuant to title 17 Section 105 of the United
States Code this software is not subject to copyright protection and
is in the public domain. PROP is an experimental system. NIST
assumes no responsibility whatsoever for its use by other parties, and
makes no guarantees, expressed or implied, about its quality,
reliability, or any other characteristic. We would appreciate
acknowledgement if the software is used.  This software can be
redistributed and/or modified freely provided that any derivative
works bear some notice that they are derived from it, and any modified
versions bear some notice that they have been modified.
