package forks::BerkeleyDB::shared::array;

$VERSION = 0.01;
use strict;
use warnings;
use BerkeleyDB 0.27;
use vars qw(@ISA);
@ISA = qw(BerkeleyDB::Recno);

#---------------------------------------------------------------------------
sub new {
	my $type = shift;
	my $class = ref($type) || $type;
	my $self = $class->SUPER::new(@_);
	return undef unless defined $self;
	return bless($self, $class);
}

# standard Perl feature methods implemented:
#	TIEARRAY
#	FETCH, STORE
#	FETCHSIZE, STORESIZE
#	EXTEND
#	EXISTS, DELETE
#	CLEAR
#	PUSH, POP
#	SHIFT, UNSHIFT
#	SPLICE
#	UNTIE, DESTROY

#---------------------------------------------------------------------------
*TIEARRAY = *TIEARRAY = \&new;

sub _exists_elem ($) {
	my $value = shift;
	return defined $value && UNIVERSAL::isa($value, 'forks::BerkeleyDB::shared::Elem::NotExists') ? 0 : 1;
}

sub _db_filter_array_elem_not_exists_to_undef ($) {
	my $value = shift;
	return _exists_elem($value) ? $value : undef;
}

#---------------------------------------------------------------------------
sub FETCH { 
	my $self = shift;
	return undef unless @_;
	my $value = $self->SUPER::FETCH(@_);
	return _db_filter_array_elem_not_exists_to_undef($value);
}

sub STORE { 
	my $self = shift;
	my $key = shift;
	return undef unless @_;
	my $value = shift;
	{
		no warnings 'uninitialized';
		return undef unless $self->db_put($key, $value) == 0;
	}
	return $value; 
}

#---------------------------------------------------------------------------
sub FETCHSIZE {
	my $self = shift;
	my $stat = $self->db_stat();
	return defined $stat->{bt_nkeys} && $stat->{bt_nkeys} > 0 ? $stat->{bt_nkeys} : 0;
}

sub STORESIZE {
	my $self = shift;
	my $count = shift;
	my $nkeys = $self->FETCHSIZE();
#warn "STORESIZE: count=$count; nkeys=$nkeys";
	if ($nkeys < $count) { #add undef elements
		my $value = forks::BerkeleyDB::shared::Elem::NotExists->new();
		$self->db_put($_, $value, DB_APPEND) for ($nkeys..($count - 1));
	}
	elsif ($nkeys > $count) { #trim elements
		my $value = undef;
		my $cursor = $self->db_cursor(DB_WRITECURSOR);
		for (($count - 1)..($nkeys - 1)) {
			return $self->FETCHSIZE() unless $cursor->c_get($_, $value, DB_LAST) == 0;	#optimized: using DB_LAST prevents database renumbering
			return $self->FETCHSIZE() unless $cursor->c_del() == 0;
		}
	}
	return $self->FETCHSIZE();
}

#---------------------------------------------------------------------------
sub EXTEND {
	return $_[1];	#no need for pre-allocation
}

#---------------------------------------------------------------------------
sub EXISTS {	#test that this works after delete
	my $self = shift;
	my $key = shift;
	my $value = undef;
	return 0 unless $self->db_get($key, $value) == 0;
	return _exists_elem($value) ? 1 : 0;
}

sub DELETE {	#doesn't appear to support deleting entire array (delete @a[0..$#a] == DB truncate)?
	my $self = shift;
	return undef unless @_;
	my $key = shift;
	my $value = undef;
#warn "DELETE: key=$key";
	my $cursor = $self->db_cursor(DB_WRITECURSOR);
	return undef unless $cursor->c_get($key, $value, DB_SET) == 0;	#set cursor position
	if ($key == $self->FETCHSIZE() - 1) {	#if this is last key, delete element
		return undef unless $cursor->c_del() == 0;
	}
	else { #initialize element to "not exists" state
		my $new_value = forks::BerkeleyDB::shared::Elem::NotExists->new();
#warn "DELETE: success!";
		return undef unless $cursor->c_put($key, $new_value, DB_CURRENT) == 0;
	}
	
	### delete any other "not exists" elements, starting from last element ###
	my ($cur_key, $cur_value) = (0, '');
	while ($cursor->c_get($key, $value, DB_LAST) == 0) {
		if (_exists_elem($value)) {	last; }
		else {
			return undef unless $cursor->c_del() == 0;
		}
	}
	
	$cursor->c_close();
#warn "DELETE: success!";
	return _db_filter_array_elem_not_exists_to_undef($value);
}

#---------------------------------------------------------------------------
sub CLEAR {
	my $self = shift;
	my $count = 0;
	$self->truncate($count);
	return defined $count && $count > 0 ? 1 : 0;
}

#---------------------------------------------------------------------------
sub PUSH {
	my $self = shift;
	my $key = 0;
	foreach (@_) {
		return $self->FETCHSIZE() unless $self->db_put($key, $_, DB_APPEND) == 0;
	}
	return $self->FETCHSIZE();
}

sub POP {
	my $self = shift;
	my $value = $self->SUPER::POP(@_);
	return _db_filter_array_elem_not_exists_to_undef($value);
}

#---------------------------------------------------------------------------
sub SHIFT {
	my $self = shift;
	my $value = $self->SUPER::SHIFT(@_);
	return _db_filter_array_elem_not_exists_to_undef($value);
}

sub UNSHIFT {
	my $self = shift;
	return undef unless @_;
	$self->SUPER::UNSHIFT(@_);
	return $self->FETCHSIZE();
}

#---------------------------------------------------------------------------
sub SPLICE {
	my $self = shift;
	my $offset = shift || 0;
	my $length = shift;
	
	my $nkeys = $self->FETCHSIZE();
	my $p_offset = $offset < 0 ? $nkeys + $offset : $offset;
	$p_offset = $nkeys - 1 if $p_offset > $nkeys - 1;
	
	### handle warnings ###
	unless (defined $length) {
		warnings::warnif('uninitialized', 'Use of uninitialized value in splice');
		$length = $nkeys - $offset;
	}
	warnings::warnif('misc', 'splice() offset past end of array') if $offset > $nkeys - 1;
	die "Modification of non-creatable array value attempted, subscript $offset"
		if $offset < 0 && abs($offset) > $nkeys;

	### remove elements ###
	my @removed;
#warn "length=$length";		
	if ($length > 0) {
		my $cursor = $self->db_cursor(DB_WRITECURSOR);
		my $max_idx = $p_offset + $length - 1 > $nkeys - 1 ? $nkeys - 1 : $p_offset + $length - 1;
		for ($p_offset..$max_idx) {
			my $key = $p_offset;
			my $value = undef;
			my $status = $cursor->c_get($key, $value, DB_SET) == 0;	#set cursor position
			next if $status == DB_NOTFOUND || $status == DB_KEYEMPTY;
			push @removed, _db_filter_array_elem_not_exists_to_undef($value);
			return @removed unless $cursor->c_del() == 0;
			$nkeys--;
		}
		$cursor->c_close();
	}
	
	### insert elements ###
	if (@_) {
		my $num_vals = scalar @_;
#warn "num_vals to insert=$num_vals";		
		### extend database to new size ###
		$nkeys = $self->STORESIZE($nkeys + $num_vals);
#warn "new size=",$nkeys;		
		
		### insert elements starting at offset (and temporarily save old ones) ###
		my @values_to_move;
		my $cursor = $self->db_cursor(DB_WRITECURSOR);
		my $max_idx = $p_offset + ($num_vals - 1) > $nkeys - 1 ? $nkeys - 1 : $p_offset + ($num_vals - 1);
#warn "insert: range=$p_offset..$max_idx";		
		for my $key ($p_offset..$max_idx) {
			my $value = undef;
			my $new_value = shift;
			my $status = $cursor->c_get($key, $value, DB_SET) == 0;	#set cursor position
			next if $status == DB_NOTFOUND || $status == DB_KEYEMPTY;
			push @values_to_move, $value;
#warn "insert: key=$key";		
			return @removed unless $cursor->c_put($key, $new_value, DB_CURRENT) == 0;
#warn "insert success! (status=$status)";
		}
		
		### move elements shifted by splice ###
#warn "move: values=(",join(',', @values_to_move),")";
#warn "move: range=",($p_offset + $num_vals)."..".($nkeys - 1);		
		for my $key (($p_offset + $num_vals)..($nkeys - 1)) {
#warn "move: key=$key";		
			my $value = undef;
			my $status = $cursor->c_get($key, $value, DB_SET) == 0;	#set cursor position
			next if $status == DB_NOTFOUND || $status == DB_KEYEMPTY;
			push @values_to_move, $value;
			return @removed unless $cursor->c_put($key, shift @values_to_move, DB_CURRENT) == 0;
		}
		$cursor->c_close();
	}
	
	return @removed;
}

#---------------------------------------------------------------------------
sub UNTIE {
	my $self = shift;
	my $status = eval { $self->db_sync(); };
	$status = eval { $self->db_close(); };
	return defined $status && $status == 0 ? 0 : 1;
}

sub DESTROY {
	my $self = shift;
	my $status = eval { $self->db_sync(); };
	$status = eval { $self->db_close(); };
	return defined $status && $status == 0 ? 0 : 1;
}

#---------------------------------------------------------------------------

1;

__END__
=pod

=head1 NAME

forks::BerkeleyDB::shared::hash - class for tie-ing arrays to BerkeleyDB Recno

=head1 DESCRIPTION

Helper class for L<forks::BerkeleyDB::shared>.  See documentation there.

=head1 AUTHOR

Eric Rybski <rybskej@yahoo.com>.

=head1 COPYRIGHT

Copyright (c) 2006 Eric Rybski <rybskej@yahoo.com>.
All rights reserved.  This program is free software; you can redistribute it
and/or modify it under the same terms as Perl itself.

=head1 SEE ALSO

L<forks::BerkeleyDB::shared>, L<forks::shared>.

=cut
