package Plucene::Search::BooleanScorer;

=head1 NAME 

Plucene::Search::BooleanScorer - A boolean scorer

=head1 SYNOPSIS

	# isa Plucene::Search::Scorer

	$bool_scorer->add($scorer, $required, $prohibited);
	$bool_scorer->score($results, $max_doc);

=head1 DESCRIPTION

This is a scoring class for boolean scorers.

=head1 METHODS

=cut

use strict;
use warnings;

use List::Util qw(min);

use Plucene::Search::Similarity;

use base qw(Plucene::Search::Scorer Class::Accessor::Fast);

__PACKAGE__->mk_accessors(
	qw(next_mask required_mask prohibited_mask max_coord scorers bucket_table
		coord_factors current_doc)
);

=head2 new

	my $bool_scorer = Plucene::Search::BooleanScorer->new;

Create a new Plucene::Search::BooleanScorer object.

=head2 next_mask / required_mask / prohibited_mask max_coord / scorers / 
	bucket_table / coord_factors / current_doc

Get / set these attributes

=cut

sub new {
	my $self = shift->SUPER::new(@_);
	$self->max_coord(1);
	$self->next_mask(1);
	$self->current_doc(0);
	$self->required_mask(0);
	$self->prohibited_mask(0);
	$self->scorers([]);
	$self->bucket_table(Plucene::Search::BucketTable->new({ scorer => $self }));
	return $self;
}

=head2 add

	$bool_scorer->add($scorer, $required, $prohibited);

=cut

sub add {
	my ($self, $scorer, $required, $prohibited) = @_;
	my $mask = 0;
	if ($required || $prohibited) {
		$mask = $self->next_mask;
		$self->{next_mask} <<= 1;
	}

	$self->{max_coord}++ unless $prohibited;

	$self->{prohibited_mask} |= $mask if $prohibited;
	$self->{required_mask}   |= $mask if $required;
	push @{ $self->{scorers} },
		{
		scorer     => $scorer,
		required   => $required,
		prohibited => $prohibited,
		collector  => $self->bucket_table->new_collector($mask) };
}

sub _compute_coord_factors {
	my $self = shift;
	$self->coord_factors([
			map Plucene::Search::Similarity->coord($_, $self->max_coord),
			0 .. $self->max_coord
		]);
}

=head2 score

	$bool_scorer->score($results, $max_doc);

=cut

sub score {
	my ($self, $results, $max_doc) = @_;
	$self->_compute_coord_factors if not defined $self->coord_factors;
	while ($self->current_doc < $max_doc) {
		$self->current_doc(
			min(
				$self->{current_doc} + $Plucene::Search::BucketTable::SIZE, $max_doc
			));
		for my $t (@{ $self->{scorers} }) {
			$t->{scorer}->score($t->{collector}, $self->current_doc);
		}
		$self->bucket_table->collect_hits($results);
	}
}

package Plucene::Search::BucketTable;
our $SIZE = 1 << 10;
our $MASK = $SIZE - 1;

use base 'Class::Accessor::Fast';
__PACKAGE__->mk_accessors(qw(buckets first scorer));

sub new {
	my $self = shift->SUPER::new(@_);
	$self->buckets([]);
	$self;
}

sub collect_hits {
	my ($self, $results) = @_;
	my $scorer     = $self->scorer;
	my $required   = $scorer->required_mask;
	my $prohibited = $scorer->prohibited_mask;
	my @coord      = @{ $scorer->coord_factors };
	for my $bucket (@{ $self->{buckets} || [] }) {
		next unless exists $bucket->{doc};
		$results->collect($bucket->{doc},
			$bucket->{score} * $coord[ $bucket->{coord} ])
			if !($bucket->{bits} & $prohibited)
			and ($bucket->{bits} & $required) == $required;
	}
}

sub new_collector {
	my ($self, $mask) = @_;
	return Plucene::Search::BucketCollector->new({
			bucket_table => $self,
			mask         => $mask
		});
}

package Plucene::Search::BucketCollector;
use base (qw(Class::Accessor::Fast Plucene::Search::HitCollector));

__PACKAGE__->mk_accessors(qw(bucket_table mask));

sub collect {
	my ($self, $doc, $score) = @_;
	my $table  = $self->bucket_table;
	my $i      = $doc & $Plucene::Search::BucketTable::MASK;
	my $bucket = $table->buckets->[$i];
	$table->buckets->[$i] = $bucket = {} unless $bucket;

	if (not defined $bucket->{doc} or $bucket->{doc} != $doc) {
		@{$bucket}{qw(doc    score  bits         coord)} =
			($doc, $score, $self->mask, 1);
	} else {
		$bucket->{score} += $score;
		$bucket->{bits} |= $self->mask;
		$bucket->{coord}++;
	}
}

1;

