package PROP::ResultSet::Object;

use strict;
use DBI;
use PROP::Constants;
use Hash::Util qw/lock_keys/;
use PROP::SQL::Select;
use Data::Dumper;
use PROP::Query::Object;
use Data::Dumper;
use UNIVERSAL qw/isa/;

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

    my $err_msg;

    $err_msg = "was expecting a PROP::Query::Object object as first argument"
	unless isa($object_query, 'PROP::Query::Object');

    if($err_msg) {
	my ($pkg, $file, $line) = caller();
	die new PROP::Exception::IllegalArgument($err_msg, $file, $line);
    }

    $self->{-object_query} = $object_query;
    $self->{-link_result_sets} = [map { new PROP::ResultSet::Link($_) }
				  @{$object_query->get_link_queries()}];

    $self->{-stmt} = undef;
    $self->{-sth} = undef;
    $self->{-lower_bound} = 0;
    $self->{-done} = 0;
    $self->{-returned_rows} = 0;
    $self->{-limit} = 0;

    lock_keys(%$self) if DEBUG;

    my $limit = $object_query->get_limit();
    if($limit) {
	if(ref($limit)) {
	    $self->{-lower_bound} = $limit->[0];
	    $self->{-limit} = $limit->[1];
	}
	else {
	    $self->{-limit} = $limit;
	}
    }

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

    return $self;
}

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

    my (@results, $result);
    push(@results, $result) while($result = $self->get_next_result());
    return @results;
}

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

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

    my @row = $self->{-sth}->fetchrow_array();

    unless(@row) {
	if($self->{-returned_rows} < $self->{-object_query}->get_buffer_size()) {
	    $self->{-done} = 1;
	    return undef;
	}
	else {
	    $self->execute_next_statement();
	    @row = $self->{-sth}->fetchrow_array();

	    unless(@row) {
		$self->{-done} = 1;
		return undef;
	    }
	}
    }

    $self->{-returned_rows}++;
    $self->{-done} = 1 if $self->{-returned_rows} == $self->{-limit};
    return $self->instantiate_object(\@row);
}

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

    my $obj = $self->{-object_query}->get_class()->new();
    $obj->set_field_value($_, shift(@$row)) foreach($self->{-object_query}->get_class()->get_field_names());
    $obj->_clear_modification_flags();

    foreach my $result_set (@{$self->{-link_result_sets}}) {
	my $instance = $result_set->get_next_result();

	next unless $instance;

	while($instance->get_pk_value() < $obj->get_pk_value()) {
	    last unless $instance;
	    $instance = $result_set->get_next_result();
	}

	next unless $instance;

	if($instance->get_pk_value() == $obj->get_pk_value()) {
	    if($result_set->get_relationship() eq 'parents') {
		foreach my $parent (@{$instance->get_relatives()}) {
		    $obj->_add_parent($result_set->get_link(), $parent);
		}
	    }
	    else {
		foreach my $child (@{$instance->get_relatives()}) {
		    $obj->_add_child($result_set->get_link(), $child);
		}
	    }
	}
	elsif($instance->get_pk_value() > $obj->get_pk_value) {
	    $result_set->put_last_result($instance);
	}
	else {
	    my $msg = "ok, that is weird... probably an algorithm bug :-(";
	    die new PROP::Exception($msg);
	}
    }

    return $obj;
}

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

    my $class = $self->{-object_query}->get_class();

    my @field_names = $class->get_schema(PROP::DBH->get_handle())->get_field_names();

    my $stmt = new PROP::SQL::Select;

    $stmt->set_distinct();

    $stmt->add_table($class->get_table_name() . ' o');

    $stmt->push_field('o.' . $_)  foreach (@field_names);

    foreach ($self->{-object_query}->get_local_conditions()->get_expressions()) {
	my $expression = $_;

	# prepend the table alias to the conditions that reference
	# fields without the alias
	foreach my $field (@field_names) {
	    $expression =~ s/(?<!o\.)$field/o\.$field/g;
	}

	$stmt->push_conditional_expression($expression);
    }

    my $i = 1;
    foreach my $foreign_conditions (@{$self->{-object_query}->get_foreign_conditions()}) {
	my $foreign_alias = 't' . $i;
	my $link_alias = 'l' . $i;

	$stmt->add_table($foreign_conditions->get_link()->get_table_name() .
			 ' ' . $link_alias);

	if($foreign_conditions->get_relationship() eq 'parents') {
	    $stmt->add_table($foreign_conditions->get_link->get_parent_table_name()
			     . ' ' . $foreign_alias);


	    # join the tables

	    $stmt->push_conditional_expression('o.' . $class->get_pk_name() . '=' .
					       $link_alias . '.' . 
					       $foreign_conditions->get_link()->get_child_field_name());

	    $stmt->push_conditional_expression($foreign_alias .
					       '.' .
					       $foreign_conditions->get_link()->get_parent_class()->get_pk_name() .
					       '=' .
					       $link_alias . '.' . 
					       $foreign_conditions->get_link()->get_parent_field_name());

	    foreach (@{$foreign_conditions->get_expressions()}) {
		my $expression = $_;
		$expression =~ s/c\./o\./g;
		$expression =~ s/p\./$foreign_alias\./g;
		$expression =~ s/l\./$link_alias\./;
		$stmt->push_conditional_expression($expression);
	    }
	}
	else {
	    $stmt->add_table($foreign_conditions->get_link()->get_child_table_name()
			     . ' ' . $foreign_alias);

	    # join the tables

	    $stmt->push_conditional_expression('o.' . $class->get_pk_name() . '=' .
					       $link_alias . '.' . 
					       $foreign_conditions->get_link()->get_parent_field_name());

	    $stmt->push_conditional_expression($foreign_alias .
					       '.' .
					       $foreign_conditions->get_link()->get_child_class()->get_pk_name() .
					       '=' .
					       $link_alias . '.' . 
					       $foreign_conditions->get_link()->get_child_field_name());

	    foreach (@{$foreign_conditions->get_expressions()}) {
		my $expression = $_;
		$expression =~ s/p\./o\./g;
		$expression =~ s/c\./$foreign_alias\./g;
		$expression =~ s/l\./$link_alias\./;
		$stmt->push_conditional_expression($expression);
	    }
	}

	++$i;
    }

    foreach (@{$self->{-object_query}->get_orderings()}) {
	$stmt->push_ordering(/^o\./ ? $_ : 'o.' . $_);
    }

#    print "stmt: ", $stmt->stringify(), "\n";
    $self->{-stmt} = $stmt;
}

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

    $self->{-returned_rows} = 0;

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

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

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

    my @bindings = $self->{-object_query}->get_local_conditions()->get_bindings();

    foreach my $foreign_conditions (@{$self->{-object_query}->get_foreign_conditions()}) {
	push(@bindings, @{$foreign_conditions->get_bindings()});
    }

    $self->{-sth}->execute(@bindings);
}

1;

=head1 Name

PROP::ResultSet::Object

=head1 Description

Objects of this class are used to instantiate the results of a query
specified by the PROP::Query::Object object passed to its constructor.
The actual results can be obtained by repeated invocation of
get_next_result() which will return an instance of a subclass of
PROP::Object as long as there are more results, and eventually undef.

=head1 Synopsis

 $qo  = new PROP::Query::Object(...);
 $rso = new PROP::ResultSet::Object($qo);

 while($result = $rso->get_next_result()) {
     # do stuff with $result, which ISA PROP::Object
 }

=head1 Methods

=over

=item new

 $rso = new PROP::ResultSet::Object($qo);

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

=item get_next_result

 $result = $rso->get_next_result()

This method returns the next object in the result set.  The returned
object ISA PROP::Object, and is an instance of the subclass that was
specified in the contructor of the PROP::Query::Object object that was
passed to the constructor of this PROP::ResultSet::Object 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.
