package blx::xsdsql::IStream;
use strict;
use warnings;
use Carp;
use base qw(Exporter);

use overload "<>" => \&get_line;
#warning - exist a bug into package overload 
#calling @y=<$x> the operator <> return a one line (wantarray is false)

my  %t=( overload => [ ( "<>" ) ]);
our %EXPORT_TAGS=( all => [ map { @{$t{$_}} } keys %t ],%t); 
our @EXPORT_OK=( @{$EXPORT_TAGS{all}} );
our @EXPORT=qw( );

sub _pop_str {  #pop n chars from str pointer
	my ($p,$n)=@_;
	my $l=length($$p);
	my $s=substr($$p,$l - $n);
	$$p=substr($$p,0,$l - $n);
	return defined wantarray ? $s : undef;
}

sub _push_str { # push s to str pointer
	my ($p,$s,$maxsize)=@_;
	my $r=$$p.$s;
	$$p=substr($r,length($r) - $maxsize);	
	return defined wantarray ? $$p : undef;
} 

sub _init_input_stream {
	my ($self,%params)=@_;
	return $self unless defined $self->{INPUT_STREAM}; 
	my $r=ref($self->{INPUT_STREAM});
	if ($r eq '') { #string
		$self->{I}->{P}=0;  #current position
	}
	elsif ($r eq 'ARRAY') {
		$self->{I}={ R => 0,P => 0};  #current index + current position
	}
	elsif ($r eq 'SCALAR') { #reference to scalar
		$self->{I}->{P}=0;  #current index
	}
	elsif ($self->{INPUT_STREAM} eq *STDIN || $r eq 'GLOB') {
		$params{MAX_PUSHBACK_SIZE}=0 unless defined $params{MAX_PUSHBACK_SIZE};
		croak $params{MAX_PUSHBACK_SIZE}.': invalid param value MAX_PUSHBACK_SIZE'
			unless $params{MAX_PUSHBACK_SIZE}=~/^\d+$/;
		my $s='';
		$self->{BUFFER}=\$s;
		$self->{PUSHBACK_N}=0;
		$self->{MAX_PUSHBACK_SIZE}=$params{MAX_PUSHBACK_SIZE};
	}
	elsif ($r eq 'CODE') {
		#empty
	}
	else {
		croak $r.': type non implemented';
	}
	return $self;
}


sub new {
	my ($class,%params)=@_;
	my $max_pushback_size=delete $params{MAX_PUSHBACK_SIZE};
	my $self=bless(\%params,$class);
	return $self->_init_input_stream(MAX_PUSHBACK_SIZE => $max_pushback_size);
}


sub set_input_descriptor {
	my ($self,$fd,%params)=@_;
	$self->{INPUT_STREAM}=$fd;
	return $self->_init_input_stream(%params);
}


sub get_chars { 
	my ($self,$n,%params)=@_;
	$n=1 unless defined $n;
	my $stream=$self->{INPUT_STREAM};
	croak "INPUT_STREAM non set" unless defined $stream;
	croak "$n: invalid first param value" unless $n=~/^\d+$/;
	return '' if $n == 0;
	my $r=ref($stream);
	if ($stream eq *STDIN || ref($stream) eq 'GLOB') {
		my $outs='';
		if ($self->{PUSHBACK_N}) {
			my $m=$n > $self->{PUSHBACK_N} ? $self->{PUSHBACK_N} : $n;
			$outs=_pop_str($self->{BUFFER},$m);
			$n -= $m;
			$self->{PUSHBACK_N}-=$m;  
		}
		if ($n) {
			my $s=undef;
			my $r=read $stream,$s,$n;
			croak "$!" unless defined $r;
			$s='' if $r == 0;
			if ($self->{MAX_PUSHBACK_SIZE} && length($s) > 0) {
				_push_str($self->{BUFFER},$s,$self->{MAX_PUSHBACK_SIZE});				
			}
			$outs.=$s;
		}
		return $outs;
	}
	elsif ($r eq '') { #string
		return  '' if  $self->{I}->{P} >= length($stream);
		my $s=substr($stream,$self->{I}->{P},$n);
		$self->{I}->{P} += $n;
		return $s;
	}
	elsif ($r eq 'ARRAY') {
		return '' if $self->{I}->{R} >= scalar(@{$self->{INPUT_STREAM}});
		my $s='';
		while($self->{I}->{R} < scalar(@$stream)  && length($s) < $n) {
			my $e=$self->{INPUT_STREAM}->[$self->{I}->{R}];
			$e='' unless defined $e;
			$s .= "\n" if $self->{I}->{P} == 0 && $self->{I}->{R} > 0;
			my $m=$n - length($s);
			$s .= substr($e,$self->{I}->{P},$m);	
			$self->{I}->{P} += $m;
			if ($self->{I}->{P}  >= length($e)) {
				$self->{I}->{P} = 0;
				++$self->{I}->{R};
			}
		}
		return $s;
	}
	elsif ($r eq 'CODE') {
		return $stream->($self,@_);
	}
	elsif ($r eq 'SCALAR') {
		return  '' if  $self->{I}->{P} >= length($$stream);
		my $s=substr($$stream,$self->{I}->{P},$n);
		$self->{I}->{P} += $n;
		return $s;
	}
	else {
		croak $r.': type non implemented';
	}
	return undef;	
}

sub get_char {
	my ($self,%params)=@_;
	return $self->get_chars(1,%params);
}

sub get_line {
	my $self=shift;
	return <$self> if ref($self) ne  'blx::xsdsql::IStream';
	my $stream=$self->{INPUT_STREAM};
	croak "INPUT_STREAM non set" unless defined $stream;
	my $r=ref($stream);
	if (wantarray) {
		if ($stream eq *STDIN || $r eq 'GLOB') { 	#use the optimized version for file descriptor
			confess "push back not implemented for get_line " if  $self->{PUSHBACK_N};
			return <$stream>;
		} 
		my @s=();
		while(my $s=$self->get_line) {
			push @s,$s;
		}
		return @s;
	}

	if ($stream eq *STDIN || $r eq 'GLOB') {
		confess "push back not implemented for get_line " if $self->{PUSHBACK_N};
		my $s=<$stream>;
		return $s;
	}
	elsif ($r eq '') { #string
		return undef if $self->{I}->{P} >= length($stream);
		my $s='';
		while($self->{I}->{P} < length($stream)) {
			my $c=substr($stream,$self->{I}->{P}++,1);
			$s .= $c;
			last if $c eq "\n";
		}			
		return $s;
	}
	elsif ($r eq 'ARRAY') {
		return undef if $self->{I}->{R} >= scalar(@$stream);
		return 	$stream->[$self->{I}->{R}++]."\n";
	}
	elsif ($r eq 'CODE') {
		confess "push back not implemented for get_line " if $self->{PUSHBACK_N};
		return $stream->($self,@_);
	}
	elsif ($r eq 'SCALAR') {
		return undef if $self->{I}->{P} >= length($$stream);
		my $s='';
		while($self->{I}->{P} < length($$stream)) {
			my $c=substr($$stream,$self->{I}->{P}++,1);
			$s .= $c;
			last if $c eq "\n";
		}			
		return $s;
	}
	else {
		croak $r.': type non implemented';
	}
	return undef;	
}

sub push_back {
	my ($self,$n,%params)=@_;
	my $stream=$self->{INPUT_STREAM};
	croak "INPUT_STREAM non set" unless defined $stream;
	$n=1 unless defined $n;
	croak "$n: invalid first param value" unless $n=~/^\d+$/;
	my $r=ref($stream);
	if ($stream eq *STDIN || ref($stream) eq 'GLOB') {
		$self->{PUSHBACK_N} += $n;
		croak "pushback oveflow " if $self->{PUSHBACK_N} > $self->{MAX_PUSHBACK_SIZE}
			or $self->{PUSHBACK_N} > length(${$self->{BUFFER}})
	}
	elsif ($r eq '') { #string
		$self->{I}->{P} -= $n;
		croak $n.": invalid value for push_back" if $self->{I}->{P} < 0; 
	}
	elsif ($r eq 'ARRAY') {
		confess "push_back not implemented for ARRAY";
	}
	elsif ($r eq 'CODE') {
		confess "push_back not implemented for CODE";
	}
	elsif ($r eq 'SCALAR') {		
		$self->{I}->{P} -= $n;
		croak $n.": invalid value for push_back" if $self->{I}->{P} < 0; 
	}
	else {
		croak $r.': type non implemented';
	}
	return $self;
}

if (__FILE__ eq $0 || $ENV{REGRESSION_TEST}) {

	my @arr=();
	my $streamer=blx::xsdsql::IStream->new(INPUT_STREAM => \@arr);
	my $s=$streamer->get_chars(1);
	confess "check failed " if  length($s); 

	@arr= qw(pippo pluto paperino);
	$streamer=blx::xsdsql::IStream->new(INPUT_STREAM => \@arr);
	$s=$streamer->get_chars(10);
	confess "check failed " unless $s eq  "pippo\nplut";
	$s=$streamer->get_chars(3);
	confess "check failed " unless $s eq  "o\np";

	$s=$streamer->get_chars(3000);
	confess "check failed " unless $s eq  "aperino";

	$s=$streamer->get_chars(3000);
	confess "check failed " if length($s);
	


	my $src=join("\n",@arr);
	$streamer=blx::xsdsql::IStream->new(INPUT_STREAM => \$src);
	$s=$streamer->get_chars(10);
	confess "check failed " unless $s eq  "pippo\nplut";
	$s=$streamer->get_chars(3);
	confess "check failed " unless $s eq  "o\np";

	$s=$streamer->get_chars(3000);
	confess "check failed " unless $s eq  "aperino";

	$s=$streamer->get_chars(3000);
	confess "check failed " if  length($s);


	$streamer->set_input_descriptor(\@arr);
	my @t=$streamer->get_line;
	confess "check failed " if @t ne @arr;

	$streamer->set_input_descriptor(\@arr);
	@t=<$streamer>; 
	print STDERR  "(W) check failed - overload bug in line  ",__LINE__," - continue \n" if @t ne @arr;

	$streamer->set_input_descriptor(\$src);
	@t=$streamer->get_line;
	confess "check failed " if @t ne @arr;

}

1;

__END__



=head1  NAME

blx::xsdsql::IStream - generic iterator for string,array,file descriptor or subroutine

=cut

=head1 SYNOPSIS

use blx::xsdsql::IStream

=cut


=head1 DESCRIPTION

this package is a class - instance it with the method new


=head1 FUNCTIONS

this module defined the followed functions

new - constructor 

	PARAMS:
		INPUT_STREAMER  - an array,string,soubroutine or a file descriptor (default not set) 
		MAX_PUSHBACK_SIZE - the max size in characters for the internal buffer used by push_back and the streamer is a file descriptor
                            the default is 0		

set_input_descriptor - the first param  is a value same as INPUT_STREAMER
                       params:
                       MAX_PUSHBACK_SIZE - equal to same param of the constructor

	the method return the self object



get_chars - the first param is the number of chars to read (default 1)

	on EOF the method return a  null string
	if the first param is == 0 the method return null string
	on error throw an exception
 
get_char - equivalent to get_chars(1)
 
get_line - return a line in scalar mode or an array in array mode

	on EOF the method return a  null string
	on error throw an exception
	Note: if INPUT_STREAM is an array the line is an element of the array
	the line has the new line terminator "\n" also the result of <> iterator

push_back - push character into the streamer
            the first param is a number of characters to push back 
            the default is 1 
    the metod return the self object
    
=head1 EXPORT

None by default.


=head1 EXPORT_OK
	
<> - same as get_line

:overload - export only the overload methods 

:all  export all 

=head1 SEE ALSO

Mention other useful documentation such as the documentation of
related modules or operating system documentation (such as man pages
in UNIX), or any relevant external documentation such as RFCs or
standards.

=head1 AUTHOR

lorenzo.bellotti, E<lt>pauseblx@gmail.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2010 by lorenzo.bellotti

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

See http://www.perl.com/perl/misc/Artistic.html

=cut
