# WWW::Extractor::Generic::List
# - keeps track of items in an array
# - has only one cursor active at a time
# - keeps a list of cursors / mementos, which are kept valid even after insert / remove
# - unfortunately this needs to use array indexing for item access, no great stress

package WWW::Extractor::Generic::List;

#use WWW::Extractor::Generic::Object;
use WWW::Extractor::Generic::Cursor;
use WWW::Extractor::Generic::Predicates qw(:all);

use strict;
#use vars qw(@ISA);

#@ISA = qw|WWW::Extractor::Generic::Object|;

# create a new empty list
sub new {
    my $class = shift;
    my $self = {
	items=>[],
	marks=>{cursor=>$class->new_cursor}
    };
    return bless $self, $class;
}

sub new_cursor {
    my $class = shift;
    return new WWW::Extractor::Generic::Cursor;
}

sub cursor {
    my $self = shift;
    return $self->{marks}{cursor};
}

$WWW::Extractor::Generic::List::auto_name = '_' . 'a' x 8;

sub mark {
    my ($self, $name) = @_;
    $name = $WWW::Extractor::Generic::List::auto_name++ unless defined $name;
    $self->{marks}{$name} = copy($self->cursor);
    return $name;
}

# forget a mark
sub forget {
    my ($self, $name) = @_;
    exists $self->{marks}{$name} or die "cannot forget mark '$name'";
    delete $self->{marks}{$name};
}

sub goto {
    my ($self, $name) = @_;
    exists $self->{marks}{$name} or die "cannot goto mark '$name'";
    $self->{marks}{cursor} = copy($self->{marks}{$name});
}

# utility function for copying arbitrary data structures -
# NOT efficient as it uses Data::Dumper
# I should re-write this somehow
use Data::Dumper; # temp - needed to copy cursors
sub copy {
    my $ref = shift;
    my $copy;
    eval Data::Dumper->Dump([$ref], ['copy']);
    return $copy;
}

# get/set the item at at the cursor
sub item {
    my ($self, $v) = @_;
    my $i = $self->cursor->{index};
    return undef if ($i == -1);
    
    if (defined $v) {
	$self->{items}[$i] = $v;
    }
    return $self->{items}[$i];
}

# get the size of the list /
sub size {
    my $self = shift;
    return scalar(@{$self->{items}});
}

# insert scalar/list
sub insert {
    my ($self, $e) = @_;
    my $i = $self->cursor->{index} + 1;
    $self->splice($i, 0, $e);
    $self->next;
}

sub remove {
    my $self = shift;
    my $i = $self->cursor->{index};
    $self->splice($i, 1);
}

sub splice {
    my ($self, $i, $l, @v) = @_;
    my $n = scalar(@v);

    splice @{$self->{items}}, $i, $l, @v;

    # move marks
    my $marks = $self->{marks};
    while (my ($name, $mark) = each %{$marks}) {
	my $i2 = $mark->{index};
	if ($i2 >= $i) {
	    if ($i2 < $i + $l) {
		$mark->{index} = $i-1;
	    } else {
		$mark->{index} = $i2 + $n - $l;
	    }
	}
    }
}

# rudimentary as_string
sub as_string {
    my $self = shift;
    return join '', map {$_->as_string."\n"} @{$self->items}
}

sub position {
    my $self = shift;
    my $item = $self->item;
    $item = defined $item ?
	$item -> as_string : '----';
    return $self->cursor->as_string . " : $item\n";
}

sub home {
    my $self = shift;
    $self->reset;
    $self->next;
}

sub end {
    my $self = shift;
    $self->reset;
    $self->prev;
}

# facade for cursor motion

sub reset {
    my $self = shift;
    $self->cursor->reset;
}

sub off {
    my $self = shift;
    return $self->{marks}{cursor}->off;
}

sub next {
    my $self = shift;
    return $self->cursor->next($self);
}

sub prev {
    my $self = shift;
    return $self->cursor->prev($self);
}

sub items {
    my $self = shift;
    return $self->{items};
}

# searching subroutines

sub test {
    my ($self, $test) = @_;
    $test = Wrap($test, \&Normal);

    return $test->($self->item, $self->cursor->{stack});
}

# find returns the element found if one is found,
# else nothing.  This means, for lists of refs,
# find can be used in a boolean context.
sub find {
    my ($self, $test, $count) = @_;

    $test = PartWrap($test);

    $count = 1 unless $count;
    my $reverse = $count < 0 and $count*=-1;

    while ($reverse ? $self->prev : $self->next) {
	# test element
	if ($self->test($test)) {
	    # we found a match, so return, or else decrement the counter
	    return $self->item if $count == 1;
	    --$count;
	}
    }
    # search fails if reached the end
    return;
}

sub find_until {
    my ($self, $test, $until, @rest) = @_;
    $self->find(Or($test, $until), @rest) && ! $self->test($until);
}

sub find_while {
    my ($self, $test, $while, @rest) = @_;
    $self->find_until($test, Not($while), @rest);
}

# find_all - not currently in use
sub find_all {
    my ($self, $test) = @_;
    my @res = ();
    while ($self->find($test)) {
	push @res, $self->element;
    }
    return \@res;
}

1
