package Perl6::Value::List;

# Perl6::Value::List - implementation of Perl6 'List' class in Perl5

# ChangeLog
#
# 2005-09-05
# * removed uniq(), grep() - now implemented in p6 Prelude
# * map() uses Code->arity
# * lists have an internal buffer for unshift/push
#
# 2005-09-01
# * fixed stringification
#
# 2005-08-31
# * str() is non-destructive
#
# 2005-08-30
# * new hook 'clone'
# * some constructors implement deep cloning
# * functions that don't support deep cloning emit warnings on clone
#
# 2005-08-29
# * new methods shift_n() pop_n() - provide lazy access to sublists
# * new hook DESTROY
#
# 2005-08-27
# * new methods start() end()
#
# 2005-08-23
# * fixed stringification
#
# 2005-08-12
# * fixed map()->pop()
#
# 2005-08-11
# * Fixed string comparison to Inf portability (Windows x Linux)
# * Separate from_num_range() and from_range() constructors. 
#   - from_num_range() is a numeric range. It accepts a 'step' value.
#   - from_range() is a generic range for strings, etc. It accepts a 'celems' closure.
#   Both constructors are just new() wrappers.
# * grep(), map() don't depend on coroutines
# * Removed pair() - this module does not have access to the Pair constructor
#
# 2005-08-10
# * Removed method concat_list(), added "TODO" methods

# TODO - finish sum() - not all operations support sum() yet; 
#      - object numification is not supported yet

# TODO - finish support for unshift, push 

# TODO - List.is_lazy() could be defined with a closure; Perl6 version too
# TODO - map(), grep() could accept the optional 'celems' parameter - for kv() implementation
# TODO - is_contiguous() should test if $step == 1

use strict;
use Perl6::Value;

use constant Inf => Perl6::Value::Num::Inf;
our $VERSION = '0.01';

sub _default_stringify {
    my $self = (shift)->clone;
    my @start;
    my @end;
    my $samples = 3;
    $samples = 1000 unless $self->is_infinite;
    my $tmp = -&Inf;
    for ( 1 .. $samples ) {
        last unless $self->elems;
        $tmp = $self->shift;
        last if $tmp == Inf;
        push @start, $tmp;
        last if $tmp == -&Inf;
    }
    $tmp = Inf;
    for ( 1 .. $samples ) {
        last unless $self->elems;
        $tmp = $self->pop( $tmp );
        last if $tmp == -&Inf;
        unshift @end, $tmp;
        last if $tmp == Inf;
    }
    return '' unless @start;
    # if @start and @end intersect, don't print ".."
    if ( $self->elems == 0 ) {
        push @start, @end;
        return join( ', ', @start, @end );
    }
    return 
        join( ', ', 
        map { Perl6::Value::stringify( $_ ) } @start ) .
        ' ... ' . 
        join( ', ', 
        map { Perl6::Value::stringify( $_ ) } @end );
}

sub new {
    my $class = shift;
    my %param = @_;

    $param{cis_infinite}   = sub { $_[0]->{celems}() == Inf } 
        unless defined $param{cis_infinite};
    $param{cis_contiguous} = sub { 0 } 
        unless defined $param{cis_contiguous};
    $param{cstringify}     = \&_default_stringify 
        unless defined $param{cstringify}; 

    $param{is_lazy}        = 1 unless defined $param{is_lazy};
    unless ( defined $param{celems} ) {
        $param{celems} =
            ( defined $param{cstart} || defined $param{cend} ) ? sub { Inf } : sub { 0 }
    }
    $param{cstart}  = sub {} unless defined $param{cstart};
    $param{cend}    = sub {} unless defined $param{cend}; 
    
    $param{start}   = sub {} unless defined $param{start};
    $param{end}     = sub {} unless defined $param{end}; 

    $param{shift_n} = sub { $_[0]->shift } unless defined $param{shift_n};
    $param{pop_n}   = sub { $_[0]->pop }   unless defined $param{pop_n}; 

    $param{DESTROY} = sub {}   unless defined $param{DESTROY}; 
    $param{clone}   = sub { 
        my $self = shift;
        my $clone = bless { %$self }, ref $self;
        @{$clone->{pops}} =   @{$self->{pops}};
        @{$clone->{shifts}} = @{$self->{shifts}};
        return $clone;
    } unless defined $param{clone}; 

    $param{sum}     = sub { undef }   unless defined $param{sum}; 

    $param{shifts}  = [] unless defined $param{shifts};
    $param{pops}    = [] unless defined $param{pops};

    return bless \%param, $class;
}

sub DESTROY       { $_[0]->{DESTROY}( @_ ) }
sub clone         { $_[0]->{clone}( @_ ) }
sub elems         { $_[0]->{celems}( @_ ) }
sub is_infinite   { $_[0]->{cis_infinite}( @_ ) }
sub is_contiguous { $_[0]->{cis_contiguous}( @_ ) }
sub is_lazy       { $_[0]->{is_lazy} }
sub str           { $_[0]->{cstringify}( @_ ) }
sub int           { $_[0]->elems }
sub bit           { $_[0]->elems > 0 }
sub num           { $_[0]->elems }
sub perl          { '(' . $_[0]->{cstringify}( @_ ) . ')' }
sub shift_n       { $_[0]->{shift_n}( @_ ) }
sub pop_n         { $_[0]->{pop_n}( @_ ) }
sub sum           { $_[0]->{sum}( @_ ) }

sub flatten       { 
    my $ret = shift;
    my $class = ref($ret);

    # TODO - add tests for this error message
    # fail "can't instantiate an infinite list"
    #     if $ret->is_infinite;

    my @list;
    while ( $ret->elems ) { push @list, $ret->shift }
    $class->from_single( @list ); 
}

sub from_num_range {
    my $class = shift;
    my %param = @_;
    my $start = Perl6::Value::numify( $param{start} );
    my $end =   Perl6::Value::numify( $param{end} );
    my $step =  Perl6::Value::numify( $param{step} || 1 );
    $class->new(
        clone => sub { 
            $class->from_num_range( start => $start, end => $end, step => $step ) 
        },
        sum => sub {
            $_[0]->elems * ( $start + $end ) / 2;
        },
        shift_n => sub {
            my $list = shift;
            my $length = shift;
            # warn "shift_n( $length ) from  ". $list->start . "..". $list->end;
            my $middle = $start + $length - 1;
            $middle = $end if $middle > $end;
            my $shifted = ref($list)->from_num_range(
                start => $start,
                end => $middle,
                step => $step,
            );
            $start = $middle + 1;
            return $shifted;
        },
        start =>   sub { $start },
        end =>     sub { $end },
        cstart =>  sub {
            my $r = $start;
            if ( defined $step ) { $start += $step } else { $start++ };
            return $r;
        },
        cend =>    sub {
            my $r = $end;
            if ( defined $step ) {
                # XXX - this should use modulus, etc.
                $end -= $step
            }
            else {
                $end--
            };
            return $r;
        },
        celems =>  sub {
            # warn "ELEMS $end - $start = ".($end - $start + 1)."\n";
            return Inf if $start == -&Inf || $end == Inf;
            return $end - $start + 1 unless defined $step;
            return CORE::int(( $end - $start + 1 ) / $step);
        },
        cis_infinite =>   sub { return $start == -&Inf || $end == Inf },
        cis_contiguous => sub { $step == -1 || $step ==  1 },
    );
}

sub from_range {
    my $class = shift;
    my %param = @_;
    my $start = $param{start};
    my $end =   $param{end};
    my $count = $param{celems};
    $count = sub { 
            no warnings 'numeric';
            if ( ref($end) ) {
                return Inf if $end->unboxed == Inf;
                return $start->unboxed le $end->unboxed ? Inf : 0 
            }
            else
            {
                return Inf if $end == Inf;
                return $start le $end ? Inf : 0 
            }
        } 
        unless defined $count;
    $class->new(
        clone => sub { 
            $class->from_range( start => $start, end => $end, celems => $count ) 
        },
        sum => sub {
            warn "string sum not supported";
        },
        start =>   sub { $start },
        end =>     sub { $end },
        cstart =>  sub { 
            my $tmp = $start; 
            if ( ref( $start ) ) {
                $start = $start->increment; 
            }
            else {
                $start++;
            }
            $tmp; 
        },
        cend =>    sub { 
            my $tmp = $end; 
            if ( ref( $end ) ) {
                $end = $end->decrement; 
            }
            else {
                $end--;
            }
            $tmp; 
        },
        celems =>  $count,
        cis_contiguous => sub { 1 },
    );
}

sub from_x {
    # implements ('a' x 100) style lists
    # this can be used to create sparse arrays like:  [ 1, undef x 10000, 2 ]
    my $class = shift;
    my %param = @_;
    my $item =  $param{item};
    my $count = $param{count};
    $count = 0 
        unless defined $count;
    $class->new(
        clone => sub { 
            $class->from_x( item => $item, count => $count ) 
        },
        sum => sub {
            Perl6::Value::numify( $_[0] ) * $item;
        },
        shift_n => sub {
            my $list = shift;
            my $length = shift;
            # warn "shift_n( $length ) from  ". $list->start . "..". $list->end;
            $length = $count if $length > $count;
            my $shifted = ref($list)->from_x(
                item => $item,
                count => $length,
            );
            $count = $count - $length;
            return $shifted;
        },
        start =>   sub { $item },
        end =>     sub { $item },
        cstart =>  sub { $count--; return if $count < 0; $item },
        cend =>    sub { $count--; return if $count < 0; $item },
        celems =>  sub { $count },
    );
}

sub from_single {
    my $class = shift;

    my @list;
    for( @_ ) {
        if ( UNIVERSAL::isa($_, 'Perl6::Value::List') ) {
            my @li; push @li, $_->shift while $_->elems; 
            push @list, @li;
        }
        else {
            push @list, $_
        }
    }

    $class->new(
        clone =>   sub { $class->from_single( @list ) },
        start =>   sub { $list[0]  },
        end =>     sub { $list[-1] },
        cstart =>  sub { shift  @list },
        cend =>    sub { pop    @list },
        celems =>  sub { scalar @list },
        is_lazy => 0,
     );
}

sub from_coro {
    my $class = shift;
    my $start = shift;
    my $size =  &Inf;
    $class->new(
        clone => sub { 
            warn "from_coro->clone() not implemented";
            $class->from_coro( $start ) 
        },
        cstart =>  sub {
            my $r = $start->();
            # print "coro\n";
            $size = 0 unless defined $r;
            return $r;
        },
        cend =>           sub {},
        celems =>         sub { $size },
        cis_infinite =>   sub { $size == Inf },
        cis_contiguous => sub { 0 },
    );
}

# --- list operations ---

sub reverse { 
    my $ret = shift;
    Perl6::Value::List->new( 
        clone => sub { 
            $ret->clone->reverse 
        },
        sum =>            $ret->{sum},
        start =>          $ret->{end},
        end =>            $ret->{start},
        cstart =>         $ret->{cend},
        cend =>           $ret->{cstart},
        celems =>         $ret->{celems},
        cis_infinite =>   $ret->{cis_infinite},
        cis_contiguous => $ret->{cis_contiguous},
        cstringify =>     $ret->{cstringify},
    );
}

sub map { 
    my $array = shift;
    my $code = shift;
    my $ret = $array->clone; 
    my $arity = Perl6::Value::numify( $code->arity );
    Perl6::Value::List->new(
        clone => sub { 
            my $self = shift;
            # this doesn't work if $code is a closure
            # it may break grep(), uniq(), kv()...
            # It will need $code->clone in order to work...
            warn "List::map->clone() not implemented";
            warn "map->clone has pending shifts/pops" if @{$self->{pops}} || @{$self->{shifts}};
            $ret->clone->map( $code ) 
        },
        cstart => sub {
            my $self = shift;
            # print "entering map, elems = ", $ret->elems, "\n";
            while( $ret->elems && @{$self->{shifts}} < 2 ) {
                my @x;
                push @x, $ret->shift for 1 .. $arity;
                my $res = $code->do( @x );
                my @res;
                if ( Perl6::Value::p6v_isa( $res, 'Array' ) ) {
                    @res = $res->items 
                }
                else {
                    @res = ($res)
                }
                push @{$self->{shifts}}, @res;
            }
            # print " left [", @shifts, @pops, "] ", scalar @shifts, "+", scalar @pops, "\n";
            return shift @{$self->{shifts}} if @{$self->{shifts}};
            return shift @{$self->{pops}}   if @{$self->{pops}};
            return
        },
        cend => sub { 
            my $self = shift;
            while( $ret->elems && @{$self->{pops}} < 2 ) {
                my @x;
                unshift @x, $ret->pop for 1 .. $arity;
                my $res = $code->do( @x );
                my @res;
                if ( Perl6::Value::p6v_isa( $res, 'Array' ) ) {
                    @res = $res->items 
                }
                else {
                    @res = ($res)
                }
                unshift @{$self->{pops}}, @res;
            }
            return pop @{$self->{pops}}   if @{$self->{pops}};
            return pop @{$self->{shifts}} if @{$self->{shifts}};
            return
        },
        celems => sub { 
            my $self = shift;
            $ret->elems ? Inf : scalar @{$self->{shifts}} + scalar @{$self->{pops}}
        },
    );
}

sub zip { 
    my $array = shift;
    my @lists = @_;
    my $ret = $array->clone; 
    Perl6::Value::List->new(
        sum => sub { 
            my $sum = $ret->sum;
            $sum += $_->sum for @lists;
            return $sum;
        },
        clone => sub {
            my $self = shift;
            my ( $l, @ls ) = map { $_->clone } ( $ret, @lists );
            warn "zip->clone has pending shifts/pops" if @{$self->{pops}} || @{$self->{shifts}};
            return $l->zip( @ls );
        },
        cstart => sub {
            my $self = shift;
            return shift @{$self->{shifts}} if @{$self->{shifts}};
            my $any = 0;
            for ( $ret, @lists ) { $any++ if $_->elems }
            push @{$self->{shifts}}, ( $ret->shift, 
                        map { my $x = $_->shift } #; defined $x ? $x : 'x' } 
                            @lists ) if $any;
            return shift @{$self->{shifts}} if @{$self->{shifts}};
            return shift @{$self->{pops}}   if @{$self->{pops}};
            return
        },
        cend => sub { 
            my $self = shift;
            return pop @{$self->{pops}} if @{$self->{pops}};
            my $any = 0;
            for ( $ret, @lists ) { $any++ if $_->elems }
            unshift @{$self->{pops}}, ( $ret->pop, 
                        map { my $x = $_->pop } #; defined $x ? $x : 'x' } 
                            @lists ) if $any;
            return pop @{$self->{pops}}   if @{$self->{pops}};
            return pop @{$self->{shifts}} if @{$self->{shifts}};
            return
        },
        celems => sub { 
            my $self = shift;
            my $any = 0;
            for ( $ret, @lists ) { $any++ if $_->elems }
            $any ? Inf : scalar @{$self->{shifts}} + scalar @{$self->{pops}} 
        },
    );
}

sub shift { $_[0]->{celems}( @_ ) ? $_[0]->{cstart}( @_ ) : undef }
sub pop   { $_[0]->{celems}( @_ ) ? $_[0]->{cend}( @_ )   : undef }  
sub start { $_[0]->{celems}( @_ ) ? $_[0]->{start}( @_ )  : undef }
sub end   { $_[0]->{celems}( @_ ) ? $_[0]->{end}( @_ )    : undef }  

1;
__END__

# removed uniq, grep - these are just examples

sub _MySub::arity { 1 };
sub _MySub::do    { (shift)->(@_) };

sub uniq { 
    # TODO - use p6 hash
    my $array = shift;
    my %seen = ();
    return $array->map( 
        bless sub {
            my $str = $_[0];
            $str = '**UnDeF**' unless defined $str;
            return if $seen{$str};
            $seen{$str}++;
            $_[0];
        }, '_MySub' ); 
}

sub grep { 
    my $array = shift;
    my $code = shift;
    return $array->map( 
        bless sub { 
            return $_[0] if $code->($_[0]);
            return
        }, '_MySub' ); 
}

sub kv { 
    my $array = shift;
    my $count = 0;
    return $array->map( 
        sub {
            return ( $count++, $_[0] )
        } ); 
}

sub keys { 
    my $array = shift;
    my $count = 0;
    return $array->map( 
        sub {
            $count++
        } ); 
}

sub values { 
    @_
}


=head1 NAME

Perl6::Value::List - Perl extension for Perl6 "List" class

=head1 SYNOPSIS

  use Perl6::Value::List;
  
  my $list = Perl6::Value::List.from_range( start => 10, end => 20 );
  
  my $list = Perl6::Value::List.new( ... );

=head1 DESCRIPTION

This module implements a "List" object.

new() without parameters is an empty list.

=head1 SEE ALSO

Pugs

=head1 AUTHOR

Flavio S. Glock, E<lt>fglock@gmail.com<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2005 by Flavio S. Glock

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.4 or,
at your option, any later version of Perl 5 you may have available.


=cut
