package Sidef::Types::Range::RangeNumber {

    use utf8;
    use 5.016;

    use parent qw(
      Sidef::Types::Range::Range
    );

    use overload q{""} => sub {
        my ($self) = @_;
        "RangeNum(" . join(', ', $self->{from}->dump, $self->{to}->dump, $self->{step}->dump) . ")";
    };

    use Sidef::Types::Bool::Bool;
    use Sidef::Types::Number::Number;

    sub new {
        my (undef, $from, $to, $step) = @_;

        if (not defined $from) {
            $from = Sidef::Types::Number::Number::ZERO;
            $to   = Sidef::Types::Number::Number::MONE;
        }

        if (not defined $to) {
            $to   = $from->sub(Sidef::Types::Number::Number::ONE);
            $from = Sidef::Types::Number::Number::ZERO;
        }

        if (ref($to) eq __PACKAGE__) {
            $to = $to->{to};
        }

        bless {
               from => $from,
               to   => $to,
               step => $step // Sidef::Types::Number::Number::ONE,
              },
          __PACKAGE__;
    }

    *call = \&new;

    my @cache;

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

        my $step = $self->{step};
        my $from = $self->{from};
        my $to   = $self->{to};

        my $times = ($self->{_times} //= ((ref($$step) || ($$step != 1)) ? $to->sub($from)->add($step)->div($step) : $to->sub($from)->add($step)));

        if (ref($times) eq 'Sidef::Types::Number::Number') {
            my $repetitions = ref($$times) ? CORE::int(Sidef::Types::Number::Number::__numify__($$times)) : $$times;

            if ($repetitions < 0) {
                return Sidef::Types::Block::Block->new(code => sub { undef; });
            }

            if (    ref($step) eq 'Sidef::Types::Number::Number'
                and ref($from) eq 'Sidef::Types::Number::Number'
                and (!ref($$from) or ref($$from) eq 'Math::GMPz')
                and (!ref($$step) or ref($$step) eq 'Math::GMPz')) {

                $from = $$from;
                $step = $$step;

                if (    ref($to) eq 'Sidef::Types::Number::Number'
                    and (!ref($$to)  or (ref($$to) eq 'Math::GMPz' and Math::GMPz::Rmpz_fits_slong_p($$to)))
                    and (!ref($from) or Math::GMPz::Rmpz_fits_slong_p($from))
                    and (!ref($step) or Math::GMPz::Rmpz_fits_slong_p($step))) {

                    $from = Math::GMPz::Rmpz_get_si($from) if ref($from);
                    $step = Math::GMPz::Rmpz_get_si($step) if ref($step);

                    return Sidef::Types::Block::Block->new(
                        code => sub {
                            --$repetitions >= 0 or return undef;
                            my $obj = $from;
                            $from += $step;
                            bless(\$obj, 'Sidef::Types::Number::Number');
                        },
                    );
                }

                if (    (!ref($from) or Math::GMPz::Rmpz_fits_slong_p($from))
                    and (!ref($step) or Math::GMPz::Rmpz_fits_slong_p($step))) {

                    $from = Math::GMPz::Rmpz_get_si($from) if ref($from);
                    $step = Math::GMPz::Rmpz_get_si($step) if ref($step);

                    my $counter_mpz = undef;
                    my $prev        = $from;

                    return Sidef::Types::Block::Block->new(
                        code => sub {
                            --$repetitions >= 0 or return undef;
                            my $obj = $from;

                            if (    $obj < Sidef::Types::Number::Number::ULONG_MAX
                                and $obj > Sidef::Types::Number::Number::LONG_MIN) {
                                $prev = $obj;
                                $from += $step;
                                if (    $from < Sidef::Types::Number::Number::ULONG_MAX
                                    and $from > Sidef::Types::Number::Number::LONG_MIN) {
                                    return bless(\$obj, 'Sidef::Types::Number::Number');
                                }
                            }

                            # The code below handles overflow
                            $counter_mpz //=
                              ($prev < 0)
                              ? Math::GMPz::Rmpz_init_set_si($prev)
                              : Math::GMPz::Rmpz_init_set_ui($prev);

                            my $value = bless(\Math::GMPz::Rmpz_init_set($counter_mpz), 'Sidef::Types::Number::Number');

                            ref($step)      ? Math::GMPz::Rmpz_add($counter_mpz, $counter_mpz, $step)
                              : ($step < 0) ? Math::GMPz::Rmpz_sub_ui($counter_mpz, $counter_mpz, -$step)
                              :               Math::GMPz::Rmpz_add_ui($counter_mpz, $counter_mpz, $step);

                            $value;
                        }
                    );
                }

                my $counter_mpz =
                    ref($from)  ? Math::GMPz::Rmpz_init_set($from)
                  : ($from < 0) ? Math::GMPz::Rmpz_init_set_si($from)
                  :               Math::GMPz::Rmpz_init_set_ui($from);

                return Sidef::Types::Block::Block->new(
                    code => sub {
                        --$repetitions >= 0 or return undef;

                        my $value = bless(\Math::GMPz::Rmpz_init_set($counter_mpz), 'Sidef::Types::Number::Number');

                        ref($step)      ? Math::GMPz::Rmpz_add($counter_mpz, $counter_mpz, $step)
                          : ($step < 0) ? Math::GMPz::Rmpz_sub_ui($counter_mpz, $counter_mpz, -$step)
                          :               Math::GMPz::Rmpz_add_ui($counter_mpz, $counter_mpz, $step);

                        $value;
                    },
                );
            }
        }

        my $asc = ($self->{_asc} //= !!($step->is_pos // return Sidef::Types::Block::Block->new(code => sub { undef; })));

        my $tmp;
        Sidef::Types::Block::Block->new(
            code => sub {
                ($asc ? $from->le($to) : $from->ge($to)) || return undef;
                $tmp  = $from;
                $from = $from->add($step);
                $tmp;
            },
        );
    }

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

        my @list;
        my $count = 0;

        my $iter = $self->iter;

        while (1) {
            push @list, $callback->($iter->run() // last);

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

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

        $result;
    }

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

    sub sum {
        my ($self, $arg) = @_;

        if (defined($arg)) {
            goto &sum_by;
        }

        # Formula:
        #   sum(x .. y `by` z) = (floor((y - x)/z) + 1) * (z * floor((y - x)/z) + 2*x) / 2

        my ($x, $y, $z) = @{$self}{'from', 'to', 'step'};

        my $n = $y->sub($x)->div($z);

        if ($n->is_neg) {
            return Sidef::Types::Number::Number::ZERO;
        }

        state $two = Sidef::Types::Number::Number::_set_int(2);

        $n = $n->floor;
        $n->inc->mul($z->mul($n)->add($x->mul($two)))->div($two);
    }

    *Σ = \&sum;

    sub avg_by {
        my ($self, $block) = @_;
        $self->sum_by($block)->div($self->len);
    }

    sub avg {
        my ($self, $arg) = @_;

        if (defined($arg)) {
            goto &avg_by;
        }

        $self->sum->div($self->len);
    }

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

    sub prod {
        my ($self, $arg) = @_;

        if (defined($arg)) {
            goto &prod_by;
        }

        if (    $self->{step}->is_one
            and $self->{from}->is_one
            and $self->{to}->is_pos) {
            return $self->{to}->factorial;
        }

        Sidef::Types::Number::Number::prod($self->to_list);
    }

    *Π = \&prod;

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

    sub lcm {
        my ($self, $arg) = @_;

        if (defined($arg)) {
            goto &lcm_by;
        }

        if (    $self->{step}->is_one
            and $self->{from}->is_one
            and $self->{to}->is_pos) {
            return $self->{to}->consecutive_lcm;
        }

        Sidef::Types::Number::Number::lcm($self->to_list);
    }

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

    sub gcd {
        my ($self, $arg) = @_;

        if (defined($arg)) {
            goto &gcd_by;
        }

        Sidef::Types::Number::Number::gcd($self->to_list);
    }

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

        if ($self->{step}->is_one) {
            my $left  = Sidef::Types::Number::Number->new($self->{from});
            my $right = Sidef::Types::Number::Number->new($self->{to});
            return Sidef::Types::Number::Number::bsearch($left, $right, $block);
        }

        $self->to_a->bsearch($block);
    }

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

        if ($self->{step}->is_one) {
            my $left  = Sidef::Types::Number::Number->new($self->{from});
            my $right = Sidef::Types::Number::Number->new($self->{to});
            return Sidef::Types::Number::Number::bsearch_le($left, $right, $block);
        }

        $self->to_a->bsearch_le($block);
    }

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

        if ($self->{step}->is_one) {
            my $left  = Sidef::Types::Number::Number->new($self->{from});
            my $right = Sidef::Types::Number::Number->new($self->{to});
            return Sidef::Types::Number::Number::bsearch_ge($left, $right, $block);
        }

        $self->to_a->bsearch_ge($block);
    }

    sub faulhaber_sum {
        my ($self, $k) = @_;

        $k = Sidef::Types::Number::Number->new($k);

        if ($self->{step}->is_one) {

            my $left  = Sidef::Types::Number::Number->new($self->{from});
            my $right = Sidef::Types::Number::Number->new($self->{to});

            return Sidef::Types::Number::Number::faulhaber_range($left, $right, $k);
        }

        $self->lazy->map(Sidef::Types::Block::Block->new(code => sub { $_[0]->ipow($k) }))->sum;
    }

    *faulhaber = \&faulhaber_sum;

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

        if ($self->{step}->is_one) {

            my $left  = Sidef::Types::Number::Number->new($self->{from});
            my $right = Sidef::Types::Number::Number->new($self->{to});

            return Sidef::Types::Number::Number::mertens($left, $right);
        }

        $self->lazy->grep(Sidef::Types::Block::Block->new(code => sub { $_[0]->is_squarefree }))
          ->map(Sidef::Types::Block::Block->new(code => sub { $_[0]->moebius }))->sum;
    }

    sub dump {
        my ($self) = @_;
        Sidef::Types::String::String->new("$self");
    }

    *to_s   = \&dump;
    *to_str = \&dump;

    {
        no strict 'refs';

        {
            my @methods = (
                           {
                            each_name  => 'each_squarefree',
                            arr_name   => 'squarefree',
                            count_name => 'squarefree_count',
                            sum_name   => 'squarefree_sum',
                            predicate  => sub { $_[0]->is_squarefree },
                           },
                           {
                            each_name  => 'each_cubefree',
                            arr_name   => 'cubefree',
                            count_name => 'cubefree_count',
                            sum_name   => 'cubefree_sum',
                            predicate  => sub { $_[0]->is_cubefree },
                           },
                           {
                            each_name  => 'each_squarefull',
                            arr_name   => 'squarefull',
                            count_name => 'squarefull_count',
                            sum_name   => 'squarefull_sum',
                            predicate  => sub { $_[0]->is_squarefull },
                           },
                           {
                            each_name  => 'each_cubefull',
                            arr_name   => 'cubefull',
                            count_name => 'cubefull_count',
                            sum_name   => 'cubefull_sum',
                            predicate  => sub { $_[0]->is_cubefull },
                           },
                           {
                            each_name  => 'each_nonsquarefree',
                            arr_name   => 'nonsquarefree',
                            count_name => 'nonsquarefree_count',
                            sum_name   => 'nonsquarefree_sum',
                            predicate  => sub { $_[0]->is_nonsquarefree },
                           },
                           {
                            each_name  => 'each_noncubefree',
                            arr_name   => 'noncubefree',
                            count_name => 'noncubefree_count',
                            sum_name   => 'noncubefree_sum',
                            predicate  => sub { $_[0]->is_noncubefree },
                           },
                           {
                            each_name  => 'each_semiprime',
                            arr_name   => 'semiprimes',
                            count_name => 'semiprime_count',
                            sum_name   => 'semiprime_sum',
                            predicate  => sub { $_[0]->is_semiprime },
                           },
                           {
                            each_name  => 'each_squarefree_semiprime',
                            arr_name   => 'squarefree_semiprimes',
                            count_name => 'squarefree_semiprime_count',
                            sum_name   => 'squarefree_semiprime_sum',
                            predicate  => sub { $_[0]->is_squarefree_semiprime },
                           },
                           {
                            each_name  => 'each_composite',
                            arr_name   => 'composites',
                            count_name => 'composite_count',
                            sum_name   => 'composite_sum',
                            predicate  => sub { $_[0]->is_composite },
                           },
                           {
                            each_name  => 'each_prime',
                            arr_name   => 'primes',
                            count_name => 'prime_count',
                            sum_name   => 'prime_sum',
                            predicate  => sub { $_[0]->is_prime },
                           },
                           {
                            each_name  => 'each_prime_power',
                            arr_name   => 'prime_powers',
                            count_name => 'prime_power_count',
                            sum_name   => 'prime_power_sum',
                            predicate  => sub { $_[0]->is_prime_power },
                           },
                          );

            foreach my $method (@methods) {

                # Each
                if (defined($method->{each_name})) {
                    *{__PACKAGE__ . '::' . $method->{each_name}} = sub {
                        my ($self, $block) = @_;

                        if ($self->{step}->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            &{'Sidef::Types::Number::Number::' . $method->{each_name}}($left, $right, $block);
                            return $self;
                        }

                        $self->lazy->grep(Sidef::Types::Block::Block->new(code => $method->{predicate}))->each($block);
                        return $self;
                    };
                }

                # Array
                if (defined($method->{arr_name})) {
                    *{__PACKAGE__ . '::' . $method->{arr_name}} = sub {
                        my ($self) = @_;

                        if ($self->{step}->abs->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            if ($self->{step}->is_neg) {
                                return &{'Sidef::Types::Number::Number::' . $method->{arr_name}}($right, $left)->flip;
                            }

                            return &{'Sidef::Types::Number::Number::' . $method->{arr_name}}($left, $right);
                        }

                        $self->lazy->grep(Sidef::Types::Block::Block->new(code => $method->{predicate}));
                    };
                }

                # Sum
                if (defined($method->{sum_name})) {
                    *{__PACKAGE__ . '::' . $method->{sum_name}} = sub {
                        my ($self) = @_;

                        if ($self->{step}->abs->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            if ($self->{step}->is_neg) {
                                ($right, $left) = ($left, $right);
                            }

                            return &{'Sidef::Types::Number::Number::' . $method->{sum_name}}($left, $right);
                        }

                        $self->lazy->grep(Sidef::Types::Block::Block->new(code => $method->{predicate}))->sum;
                    };
                }

                # Count
                if (defined($method->{count_name})) {
                    *{__PACKAGE__ . '::' . $method->{count_name}} = sub {
                        my ($self) = @_;

                        if ($self->{step}->abs->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            if ($self->{step}->is_neg) {
                                ($right, $left) = ($left, $right);
                            }

                            return &{'Sidef::Types::Number::Number::' . $method->{count_name}}($left, $right);
                        }

                        $self->count_by(Sidef::Types::Block::Block->new(code => $method->{predicate}));
                    };
                }
            }
        }

        {
            my @methods = (
                {
                 arr_name   => 'squarefree_almost_primes',
                 each_name  => 'each_squarefree_almost_prime',
                 count_name => 'squarefree_almost_prime_count',
                 sum_name   => 'squarefree_almost_prime_sum',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_squarefree_almost_prime($k) };
                 },
                },
                {
                 arr_name  => 'carmichael',
                 each_name => 'each_carmichael',
                 predicate => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_carmichael && $_[0]->is_almost_prime($k) };
                 },
                },
                {
                 arr_name  => 'lucas_carmichael',
                 each_name => 'each_lucas_carmichael',
                 predicate => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_lucas_carmichael && $_[0]->is_almost_prime($k) };
                 },
                },
                {
                 arr_name   => 'omega_primes',
                 each_name  => 'each_omega_prime',
                 count_name => 'omega_prime_count',
                 sum_name   => 'omega_prime_sum',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_omega_prime($k) };
                 },
                },
                {
                 arr_name   => 'almost_primes',
                 each_name  => 'each_almost_prime',
                 count_name => 'almost_prime_count',
                 sum_name   => 'almost_prime_sum',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_almost_prime($k) };
                 },
                },
                {
                 arr_name   => 'powerful',
                 each_name  => 'each_powerful',
                 count_name => 'powerful_count',
                 sum_name   => 'powerful_sum',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_powerful($k) };
                 },
                },
                {
                 arr_name   => 'nonpowerfree',
                 each_name  => 'each_nonpowerfree',
                 count_name => 'nonpowerfree_count',
                 sum_name   => 'nonpowerfree_sum',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_nonpowerfree($k) };
                 },
                },
                {
                 arr_name   => 'powerfree',
                 each_name  => 'each_powerfree',
                 count_name => 'powerfree_count',
                 sum_name   => 'powerfree_sum',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_powerfree($k) };
                 },
                },
                {
                 count_name => 'smooth_count',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_smooth($k) };
                 },
                },
                {
                 count_name => 'rough_count',
                 predicate  => sub {
                     my ($k) = @_;
                     sub { $_[0]->is_rough($k) };
                 },
                },
            );

            foreach my $method (@methods) {

                # Each
                if (defined($method->{each_name})) {
                    *{__PACKAGE__ . '::' . $method->{each_name}} = sub {
                        my ($self, $k, $block) = @_;

                        $k = Sidef::Types::Number::Number->new($k);

                        if ($self->{step}->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            &{'Sidef::Types::Number::Number::' . $method->{each_name}}($k, $left, $right, $block);
                            return $self;
                        }

                        $self->lazy->grep(Sidef::Types::Block::Block->new(code => $method->{predicate}->($k)))->each($block);
                        return $self;
                    };
                }

                # Array
                if (defined($method->{arr_name})) {
                    *{__PACKAGE__ . '::' . $method->{arr_name}} = sub {
                        my ($self, $k) = @_;

                        $k = Sidef::Types::Number::Number->new($k);

                        if ($self->{step}->abs->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            if ($self->{step}->is_neg) {
                                return &{'Sidef::Types::Number::Number::' . $method->{arr_name}}($k, $right, $left)->flip;
                            }

                            return &{'Sidef::Types::Number::Number::' . $method->{arr_name}}($k, $left, $right);
                        }

                        $self->lazy->grep(Sidef::Types::Block::Block->new(code => $method->{predicate}->($k)));
                    };
                }

                # Sum
                if (defined($method->{sum_name})) {
                    *{__PACKAGE__ . '::' . $method->{sum_name}} = sub {
                        my ($self, $k) = @_;

                        $k = Sidef::Types::Number::Number->new($k);

                        if ($self->{step}->abs->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            if ($self->{step}->is_neg) {
                                ($right, $left) = ($left, $right);
                            }

                            return &{'Sidef::Types::Number::Number::' . $method->{sum_name}}($k, $left, $right);
                        }

                        $self->lazy->grep(Sidef::Types::Block::Block->new(code => $method->{predicate}->($k)))->sum;
                    };
                }

                # Count
                if (defined($method->{count_name})) {

                    *{__PACKAGE__ . '::' . $method->{count_name}} = sub {
                        my ($self, $k) = @_;

                        $k = Sidef::Types::Number::Number->new($k);

                        if ($self->{step}->abs->is_one) {

                            my $left  = Sidef::Types::Number::Number->new($self->{from});
                            my $right = Sidef::Types::Number::Number->new($self->{to});

                            if ($self->{step}->is_neg) {
                                ($right, $left) = ($left, $right);
                            }

                            return &{'Sidef::Types::Number::Number::' . $method->{count_name}}($k, $left, $right);
                        }

                        $self->count_by(Sidef::Types::Block::Block->new(code => $method->{predicate}->($k)));
                    };

                }
            }
        }

    }
}

1;
