package Sidef::Object::Lazy {

    use utf8;
    use 5.016;
    ##use overload q{""} => \&to_a;

    sub new {
        my (undef, %opt) = @_;
        bless {
               calls => [],
               %opt,
              },
          __PACKAGE__;
    }

    sub _xs {
        my ($self, $callback) = @_;

        my $iter = $self->{obj}->iter;

      ITEM: for (; ;) {
            my @arg = ($iter->run() // last);
            foreach my $call (@{$self->{calls}}) {
                @arg = $call->(@arg);
                @arg || next ITEM;
            }
            last if $callback->(@arg);
        }
    }

    sub to_a {
        my ($self) = @_;
        my @arr;
        $self->_xs(sub { push @arr, @_; 0 });
        Sidef::Types::Array::Array->new(\@arr);
    }

    sub each {
        my ($self, $block) = @_;
        $self->_xs(sub { $block->run(@_); 0 });
        $self;
    }

    sub _fast_reduce {
        my ($self, $method, $result, $callback) = @_;

        my @list;
        my $count = 0;
        my $index = 0;

        $self->_xs(
            sub {
                CORE::push(@list, $callback->($index++, @_));

                if (++$count > 1e5) {
                    $count  = 0;
                    $result = $result->$method(CORE::splice(@list));
                }
                0;    # keep looping
            }
        );

        if (@list) {
            $result = $result->$method(CORE::splice(@list));
        }

        $result;
    }

    sub reduce_by {
        my ($self, $block, $result) = @_;

        $self->_xs(
            sub {
                if (defined($result)) {
                    $result = $block->run($result, @_);
                }
                else {
                    $result = $_[0];
                }
                0;    # keep looping
            }
        );

        return $result;
    }

    sub reduce {
        my ($self, $operator, $result) = @_;

        if (ref($operator) eq 'Sidef::Types::Block::Block') {
            goto &reduce_by;
        }

        $operator = "$operator";
        $self->_xs(
            sub {
                if (defined($result)) {
                    $result = $result->$operator(@_);
                }
                else {
                    $result = $_[0];
                }
                0;    # keep looping
            }
        );
        return $result;
    }

    sub sum {
        my ($self, $block) = @_;
        $block //= Sidef::Types::Block::Block::IDENTITY;
        $self->_fast_reduce('sum', Sidef::Types::Number::Number::ZERO, sub { $block->run($_[1]) });
    }

    *sum_by = \&sum;

    sub prod {
        my ($self, $block) = @_;
        $block //= Sidef::Types::Block::Block::IDENTITY;
        $self->_fast_reduce('prod', Sidef::Types::Number::Number::ONE, sub { $block->run($_[1]) });
    }

    *prod_by = \&prod;

    sub sum_kv {
        my ($self, $block) = @_;
        $block //= Sidef::Types::Block::Block::IDENTITY;
        $self->_fast_reduce(
            'sum',
            Sidef::Types::Number::Number::ZERO,
            sub {
                $block->run(Sidef::Types::Number::Number::_set_int($_[0]), $_[1]);
            }
        );
    }

    sub prod_kv {
        my ($self, $block) = @_;
        $block //= Sidef::Types::Block::Block::IDENTITY;
        $self->_fast_reduce('prod', Sidef::Types::Number::Number::ONE, sub { $block->run(Sidef::Types::Number::Number::_set_int($_[0]), $_[1]) });
    }

    sub iter {
        my ($self) = @_;

        my $iter = $self->{obj}->iter;

        Sidef::Types::Block::Block->new(
            code => sub {
              ITEM: {
                    my $item = $iter->run() // return undef;
                    my @arg  = ($item);
                    foreach my $call (@{$self->{calls}}) {
                        @arg = $call->(@arg);
                        @arg || redo ITEM;
                    }
                    $arg[0];
                }
            }
        );
    }

    sub first {
        my ($self, $n) = @_;

        if (!defined($n)) {
            my @arr;
            $self->_xs(sub { push(@arr, @_); 1; });
            return $arr[0];
        }

        if (ref($n) eq 'Sidef::Types::Block::Block') {
            return $self->first_by($n);
        }

        $n = CORE::int($n);
        $n > 0 || return Sidef::Types::Array::Array->new([]);

        my @arr;

        $self->_xs(
            sub {
                push @arr, @_;
                @arr >= $n;
            }
        );

        Sidef::Types::Array::Array->new(\@arr);
    }

    sub nth {
        my ($self, $n) = @_;

        my @arr;
        $n = CORE::int($n);
        $n > 0 || return undef;

        $self->_xs(
            sub {
                push @arr, @_;
                @arr >= $n;
            }
        );

        $arr[$n - 1];
    }

    sub first_by {
        my ($self, $block) = @_;
        $self->grep($block)->first(1)->[0];
    }

    #
    ## Functional methods
    #

    sub grep {
        my ($self, $block) = @_;
        __PACKAGE__->new(
            obj   => $self->{obj},
            calls => [
                @{$self->{calls}},
                sub {
                    $block->run($_[0]) ? $_[0] : ();
                },
            ],
        );
    }

    *select = \&grep;

    sub while {
        my ($self, $block) = @_;

        my @arr;

        $self->_xs(
            sub {
                $block->run(@_) ? do { push(@arr, @_); 0 } : 1;
            }
        );

        Sidef::Types::Array::Array->new(\@arr);
    }

    sub map {
        my ($self, $block) = @_;
        __PACKAGE__->new(
            obj   => $self->{obj},
            calls => [
                @{$self->{calls}},
                sub {
                    $block->run($_[0]);
                }
            ],
        );
    }

    *collect = \&map;

    sub lazy {
        my ($self) = @_;
        $self;
    }
}

1;
