package Sidef::Types::Number::PolynomialMod;

# References:
#   Lecture 14, Week 8 (2hrs) - Towards polynomial factorization
#   https://youtube.com/watch?v=KNyHz0eoAMA
#
#   Lecture 15, Week 8, 1hr - Polynomial factorization
#   https://youtube.com/watch?v=tMqKqsMb-Ro

use utf8;
use 5.016;

use List::Util   qw();
use Scalar::Util qw();

use parent qw(
  Sidef::Types::Number::Polynomial
);

use overload
  q{bool} => sub { (@_) = ($_[0]); goto &__boolify__ },
  q{""}   => sub { (@_) = ($_[0]); goto &__stringify__ },
  q{%{}}  => \&to_poly,
  q{${}}  => \&Sidef::Types::Number::Polynomial::to_n,
  q{0+}   => \&Sidef::Types::Number::Polynomial::to_n;

sub new {
    my (undef, @args) = @_;

    my $mod = pop @args;

    if (Scalar::Util::reftype($mod) eq 'ARRAY') {
        $mod = __PACKAGE__->SUPER::new($mod);
    }

    if (scalar(@args) == 0 and ref($_[0]) eq __PACKAGE__) {
        return $_[0]->eval($mod);
    }

    my $poly;
    if (scalar(@args) == 1 and ref($args[0]) eq 'Sidef::Types::Number::Polynomial') {
        $poly = $args[0];
    }
    else {
        $poly = __PACKAGE__->SUPER::new(@args);
    }

    $poly = $poly->mod($mod)->lift;
    bless [$poly, $mod];
}

*call = \&new;

sub with_value {
    my ($self, $value) = @_;
    __PACKAGE__->new(0 => $value, $self->[1]);
}

sub to_poly {
    $_[0][0];
}

*lift = \&to_poly;

sub modulus {
    $_[0][1];
}

sub __dump__ {
    my ($x) = @_;
    my $str = join('', $x->[0]->dump);
    $str =~ s{^Polynomial}{PolynomialMod};
    $str =~ s{\(\)}{\(0\)};
    $str =~ s{\)\z}{, $x->[1])};
    return $str;
}

sub __boolify__ {
    my ($x) = @_;
    ($x->{0} // 0);
}

sub __stringify__ {
    my ($x, $method) = @_;
    $method //= 'to_s';
    $x->[0]->SUPER::__stringify__($method) . join('', " (mod ", ($x->[1] // '1'), ')');
}

sub to_s {
    my ($x) = @_;
    Sidef::Types::String::String->new($x->__stringify__);
}

*stringify = \&to_s;

sub pretty {
    my ($x) = @_;
    Sidef::Types::String::String->new($x->__stringify__('pretty'));
}

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

sub derivative {
    my ($x) = @_;
    my $m = $x->[1];
    $x = $x->[0];
    __PACKAGE__->new((map { (Math::Prime::Util::GMP::subint($_, 1), $x->{$_}->mul(Sidef::Types::Number::Number::_set_int($_))->mod($m)) } CORE::keys(%$x)), $m);
}

sub eval {
    my ($x, $value) = @_;

    my $m = $x->[1];
    $x = $x->[0];

    CORE::keys(%$x) || return Sidef::Types::Number::Number::ZERO;

    $value = $value->mod($m);
    Sidef::Types::Number::Number::sum(
                 map { Sidef::Types::Number::Mod->new($value, $m)->pow(Sidef::Types::Number::Number::_set_int($_))->lift->mul($x->{$_}->eval($value))->mod($m) }
                   CORE::keys(%$x))->mod($m);
}

sub binomial {
    my ($n, $k) = @_;

    my $k_int = CORE::int($k);
    my @terms;

    foreach my $i (0 .. $k_int - 1) {
        push @terms, $n;
        $n = $n->dec;
    }

    @terms || return __PACKAGE__->new((0 => Sidef::Types::Number::Number::ONE), $n->[1]);

    my $prod = Sidef::Types::Number::Number::_binsplit(\@terms, \&Sidef::Types::Number::PolynomialMod::mul);
    $prod->div($k->factorial);
}

sub chinese {
    my (@polynomials) = @_;

    my @moduli = map { $_->modulus } @polynomials;

    my $m = $moduli[0];
    foreach my $i (1 .. $#moduli) {
        $m = $m->lcm($moduli[$i]);
    }

    my $c = __PACKAGE__->new($m);

    foreach my $i (0 .. $#polynomials) {
        my $poly = $polynomials[$i];
        my $n    = $moduli[$i];
        my $t    = $m->div($n);
        my $u    = $t->mul($t->inv->mod($n));
        $c = $c->add(__PACKAGE__->new($poly->lift, $m)->mul($u));
    }

    return $c;
}

sub add {
    my ($x, $y) = @_;

    if (ref($y) eq __PACKAGE__ and $x->[1]->eq($y->[1])) {
        my $m = $x->[1];
        $x = $x->[0];
        $y = $y->[0];
        return
          __PACKAGE__->new(
                           (
                            (map { $_ => (exists($y->{$_}) ? $x->{$_}->add($y->{$_})->mod($m) : $x->{$_}) } CORE::keys %$x),
                            (map { exists($x->{$_}) ? () : ($_ => $y->{$_}) } CORE::keys(%$y))
                           ),
                           $m
                          );
    }

    my $m = $x->[1];
    $x = $x->[0];

    if (not exists $x->{0}) {
        return __PACKAGE__->new(0 => $y, %$x, $m);
    }

    $y = $y->mod($m);

    __PACKAGE__->new((map { $_ => (($_ eq '0') ? $x->{$_}->add($y)->mod($m) : $x->{$_}) } CORE::keys(%$x)), $m);
}

sub sub {
    my ($x, $y) = @_;

    if (ref($y) eq __PACKAGE__ and $x->[1]->eq($y->[1])) {
        my $m = $x->[1];
        $x = $x->[0];
        $y = $y->[0];
        return
          __PACKAGE__->new(
                           (
                            (map { $_ => (exists($y->{$_}) ? $x->{$_}->sub($y->{$_})->mod($m) : $x->{$_}) } CORE::keys %$x),
                            (map { exists($x->{$_}) ? () : ($_ => $y->{$_}->neg) } CORE::keys(%$y))
                           ),
                           $m
                          );
    }

    my $m = $x->[1];
    $x = $x->[0];

    if (not exists $x->{0}) {
        return __PACKAGE__->new(0 => $y->neg, %$x, $m);
    }

    $y = $y->mod($m);

    __PACKAGE__->new((map { $_ => (($_ eq '0') ? $x->{$_}->sub($y)->mod($m) : $x->{$_}) } CORE::keys(%$x)), $m);
}

sub mul {
    my ($x, $y) = @_;

    if (ref($y) eq __PACKAGE__ and $x->[1]->eq($y->[1])) {

        my $m = $x->[1];

        $x = $x->[0];
        $y = $y->[0];

        my @keys_x = CORE::keys %$x;
        my @keys_y = CORE::keys %$y;

        my %poly;
        foreach my $key_x (@keys_x) {
            foreach my $key_y (@keys_y) {

                my $coeff = $x->{$key_x}->mul($y->{$key_y})->mod($m);
                my $key_z = Math::Prime::Util::GMP::addint($key_x, $key_y);

                if (exists $poly{$key_z}) {
                    $poly{$key_z} = $poly{$key_z}->add($coeff)->mod($m);
                }
                else {
                    $poly{$key_z} = $coeff;
                }
            }
        }

        return __PACKAGE__->new(%poly, $m);
    }

    $y = $y->mod($x->[1]);

    __PACKAGE__->new((map { $_ => $x->[0]{$_}->mul($y)->mod($x->[1]) } CORE::keys(%{$x->[0]})), $x->[1]);
}

sub sqr {
    my ($x) = @_;
    $x->mul($x);
}

sub divmod {
    my ($x, $y) = @_;

    # Reference:
    #   https://en.wikipedia.org/wiki/Polynomial_greatest_common_divisor#Euclidean_division

    if ($x->[1]->ne($y->[1])) {
        return (__PACKAGE__->new(Sidef::Types::Number::Number::nan(), Sidef::Types::Number::Number::nan()),
                __PACKAGE__->new(Sidef::Types::Number::Number::nan()));
    }

    my $m = $x->[1];

    my $x_mod = $x;
    my $y_mod = $y;

    $x = $x->[0];
    $y = $y->[0];

    my $deg_r = List::Util::max(CORE::keys(%$x));    # deg(x)
    $deg_r // return (__PACKAGE__->new($m), __PACKAGE__->new($m));

    if ($deg_r > ~0) {
        $deg_r = Math::GMPz::Rmpz_init_set_str("$deg_r", 10);
    }

    my $deg_y = List::Util::max(CORE::keys(%$y));    # deg(y)
    $deg_y // return (__PACKAGE__->new(0 => Sidef::Types::Number::Number::inf(), $m), __PACKAGE__->new($m));

    if ($deg_y > ~0) {
        $deg_y = Math::GMPz::Rmpz_init_set_str("$deg_y", 10);
    }

    my $q = __PACKAGE__->new($m);
    my $r = $x_mod;
    my $c = $y->{$deg_y};                            # lc(y)

    # Calculate modular inverse of the leading coefficient of divisor
    my $inv_c = $c->inv->mod($m);

    # If inverse does not exist (e.g. m is not prime), return NaN
    if ($inv_c->is_nan) {
        return (__PACKAGE__->new(Sidef::Types::Number::Number::nan(), Sidef::Types::Number::Number::nan()), __PACKAGE__->new($m));
    }

    while ($deg_r >= $deg_y) {

        my $lc = $r->[0]{$deg_r};    # lc(r)

        # Fix: Use modular inverse multiplication instead of integer division
        # t = lc(r) * inv(c) (mod m)
        my $t = $lc->mul($inv_c)->mod($m);

        # s := t * x^(deg(r)−deg(y))
        my $s = __PACKAGE__->new(Math::Prime::Util::GMP::subint($deg_r, $deg_y) => $t, $m);
        $q = $q->add($s);
        $r = $r->sub($s->mul($y_mod));

        # Find deg(r) for the new r
        $deg_r = List::Util::max(CORE::keys(%{$r->[0]})) // last;

        if ($deg_r > ~0) {
            $deg_r = Math::GMPz::Rmpz_init_set_str("$deg_r", 10);
        }
    }

    return ($q, $r);
}

sub abs {
    my ($x) = @_;
    $x->mul($x->sgn);
}

sub lcm {
    my ($x, $y) = @_;
    $x->mul($y)->abs->div($x->gcd($y));
}

sub normalize_to_monic {
    my ($x) = @_;
    my $deg = $x->degree->numify;
    return $x if $deg < 0;

    my $lc = $x->[0]{$deg};

    # Already monic
    return $x if $lc->is_one;

    # Multiply polynomial by inv(lc)
    my $inv = $lc->inv->mod($x->[1]);
    return $x if $inv->is_nan;    # Should not happen in field

    $x->mul($inv);
}

sub gcd {
    my ($x, $y) = @_;

    # Reference:
    #   https://en.wikipedia.org/wiki/Polynomial_greatest_common_divisor#Euclid's_algorithm

    my $r0 = $x;
    my $r1 = $y;

    until ($r1->is_zero) {
        my $r = $r0->mod($r1);
        ($r0, $r1) = ($r1, $r);
    }

    return $r0;
}

sub monic_gcd {
    my ($x, $y) = @_;
    $x->gcd($y)->normalize_to_monic;
}

sub gcdext {
    my ($x, $y) = @_;

    # Reference:
    #   https://en.wikipedia.org/wiki/Polynomial_greatest_common_divisor#B%C3%A9zout's_identity_and_extended_GCD_algorithm

    if (ref($y) ne __PACKAGE__) {
        $y = __PACKAGE__->new(0 => $y, $x->[1]);
    }

    my $r0 = $x;
    my $r1 = $y;

    my $s0 = Sidef::Types::Number::Number::ONE;
    my $s1 = Sidef::Types::Number::Number::ZERO;
    my $t0 = Sidef::Types::Number::Number::ZERO;
    my $t1 = Sidef::Types::Number::Number::ONE;

    my $i = 1;
    until ($r1->is_zero) {
        my ($q) = $r0->divmod($r1);
        ($r0, $r1) = ($r1, $r0->sub($q->mul($r1)));
        ($s0, $s1) = ($s1, $s0->sub($q->mul($s1)));
        ($t0, $t1) = ($t1, $t0->sub($q->mul($t1)));
        ++$i;
    }

    my $g = $r0;
    my $u = $s0;
    my $v = $t0;

    my $a1 = $t1->mul(Sidef::Types::Number::Number::MONE->ipow(Sidef::Types::Number::Number::_set_int($i - 1)));
    my $b1 = $s1->mul(Sidef::Types::Number::Number::MONE->ipow(Sidef::Types::Number::Number::_set_int($i)));

    return ($g, $u, $v, $a1, $b1);
}

sub idiv {
    my ($x, $y) = @_;

    if (ref($y) ne __PACKAGE__) {
        $y = __PACKAGE__->new(0 => $y, $x->[1]);
    }

    my ($quot, $rem) = $x->divmod($y);
    return $quot;
}

# Probably not right?
*idiv_ceil  = \&idiv;
*idiv_trunc = \&idiv;
*idiv_round = \&idiv;
*idiv_floor = \&idiv;

sub div {
    my ($x, $y) = @_;

    if (ref($y) eq __PACKAGE__ and $y->[1]->eq($x->[1])) {

        my ($quot, $rem) = $x->divmod($y);

        if ($rem->is_zero) {
            return $quot;
        }

        my $frac = Sidef::Types::Number::Fraction->new($quot->mul($y)->add($rem), $y);

        if ($frac->is_zero) {
            return __PACKAGE__->new($x->[1]);
        }

        return $frac;
    }

    if ($y->is_zero) {
        return __PACKAGE__->new((0 => Sidef::Types::Number::Number::inf()), $x->[1]);
    }

    my $m = $x->[1];
    $x = $x->[0];
    $y = $y->mod($m);

    __PACKAGE__->new((map { $_ => $x->{$_}->div($y)->mod($m) } CORE::keys(%$x)), $m);
}

sub neg {
    my ($x) = @_;
    __PACKAGE__->new((map { $_ => $x->{$_}->neg } CORE::keys %{$x->[0]}), $x->[1]);
}

sub float {
    my ($x) = @_;
    __PACKAGE__->new((map { $_ => $x->{$_}->float } CORE::keys %{$x->[0]}), $x->[1]);
}

sub rat {
    my ($x) = @_;
    __PACKAGE__->new((map { $_ => $x->{$_}->rat } CORE::keys %{$x->[0]}), $x->[1]);
}

sub rat_approx {
    my ($x) = @_;
    __PACKAGE__->new((map { $_ => $x->{$_}->rat_approx } CORE::keys %{$x->[0]}), $x->[1]);
}

sub floor {
    my ($x) = @_;
    __PACKAGE__->new((map { $_ => $x->{$_}->floor } CORE::keys %{$x->[0]}), $x->[1]);
}

sub ceil {
    my ($x) = @_;
    __PACKAGE__->new((map { $_ => $x->{$_}->ceil } CORE::keys %{$x->[0]}), $x->[1]);
}

sub round {
    my ($x, $r) = @_;
    __PACKAGE__->new((map { $_ => $x->{$_}->round($r) } CORE::keys %{$x->[0]}), $x->[1]);
}

sub mod {
    my ($x, $y) = @_;

    if (ref($y) eq __PACKAGE__ and $y->[1]->eq($x->[1])) {

        # mod(a, b) = a - b * floor(a/b)
        # return $x->sub($y->mul($x->div($y)->floor));

        my ($quot, $rem) = $x->divmod($y);
        return $rem;
    }

    __PACKAGE__->new($x->[0], $y);
}

#~ sub lift {
#~ my ($x) = @_;
#~ __PACKAGE__->new((map { $_ => $x->[0]{$_}->lift } CORE::keys %{$x->[0]}), $x->[1]);
#~ }

sub inv {
    my ($x) = @_;

    my $m = $x->[1];     # modulus polynomial (a Polynomial)
    my $f = $x->lift;    # residue polynomial (a plain Polynomial)

    if (ref($m) ne 'Sidef::Types::Number::Polynomial') {
        return Sidef::Types::Number::Fraction->new(Sidef::Types::Number::Number::ONE, $x);
    }

    # Run the extended Euclidean algorithm on the plain polynomials.
    # Returns (g, u, v, ...) satisfying: u*f + v*m = g,
    # with g normalised to monic by Polynomial::gcdext.
    my ($g, $u) = $f->gcdext($m);

    # The inverse exists iff gcd(f, m) = 1.
    # If g is not the constant polynomial 1, f is not invertible mod m.
    $g->is_one
      || return __PACKAGE__->new(0 => Sidef::Types::Number::Number::nan(), $m);

    # u*f ≡ 1 (mod m).
    __PACKAGE__->new($u, $m);
}

sub is_zero {
    my ($x) = @_;
    $x->eq(Sidef::Types::Number::Number::ZERO);
}

sub is_one {
    my ($x) = @_;
    $x->eq(Sidef::Types::Number::Number::ONE);
}

sub is_mone {
    my ($x) = @_;
    $x->eq(Sidef::Types::Number::Number::MONE);
}

sub inc {
    my ($x) = @_;
    __PACKAGE__->new(%{$x->[0]}, 0 => ($x->[0]{0} // Sidef::Types::Number::Number::ZERO)->inc, $x->[1]);
}

sub dec {
    my ($x) = @_;
    __PACKAGE__->new(%{$x->[0]}, 0 => ($x->[0]{0} // Sidef::Types::Number::Number::ZERO)->dec, $x->[1]);
}

sub pow {
    my ($x, $n) = @_;

    my $m = $x->[1];

    my $negative_power = 0;

    if ($n->is_neg) {
        $n              = $n->abs;
        $negative_power = 1;
    }

    my $c = __PACKAGE__->new(0 => Sidef::Types::Number::Number::ONE, $m);

    foreach my $bit (reverse split(//, $n->as_bin)) {

        # c = (c*x)%m if bit
        # x = (x*x)%m

        $c = $c->mul($x) if $bit;
        $x = $x->mul($x);
    }

    if ($negative_power) {
        $c = $c->inv;
    }

    return $c;
}

sub cmp {
    my ($x, $y) = @_;

    if (ref($x) eq ref($y)) {
        my $mod_cmp = $x->[1]->cmp($y->[1]);

        $mod_cmp->is_zero
          or return $mod_cmp;

        $x = $x->[0];
        $y = $y->[0];
    }
    else {
        $y = $y->mod($x->[1]);
        $x = $x->[0];
    }

    return $x->cmp($y);
}

sub eq {
    my ($x, $y) = @_;

    if (ref($x) eq ref($y)) {

        $x->[1]->eq($y->[1])
          or return Sidef::Types::Bool::Bool::FALSE;

        $x = $x->[0];
        $y = $y->[0];
    }
    else {
        $y = $y->mod($x->[1]);
        $x = $x->[0];
    }

    return $x->eq($y);
}

sub ne {
    my ($x, $y) = @_;
    $x->eq($y)->not;
}

sub powmod {
    my ($base, $exp, $poly_mod) = @_;

    my $p = $base->[1];    # scalar prime (Number)

    $base = $base->mod($poly_mod);

    my $result = __PACKAGE__->new(0 => Sidef::Types::Number::Number::ONE, $p);

    foreach my $bit (reverse split(//, $exp->as_bin)) {
        $result = $result->mul($base)->mod($poly_mod) if $bit;
        $base   = $base->mul($base)->mod($poly_mod);
    }

    return $result;
}

# ------------------------------------------------------------------
# Internal helper: Distinct Degree Factorization (DDF)
#
# Given a monic squarefree polynomial f in GF(p)[x], returns a list
# of PolynomialMod objects @g where $g[k] is the product of every
# monic irreducible factor of f whose degree is exactly (k+1).
# (Factors of the wrong degree produce the constant 1 at that slot.)
#
# Algorithm:
#   Set w = x.  For k = 1, 2, ...:
#     w ← w^p mod f        (Frobenius: w accumulates x^(p^k) mod f)
#     g_k = gcd(w − x, f)  (captures all degree-k irreducibles of f)
#     f ← f / g_k
#   Stop when f = 1.
#
# Correctness: x^(p^k) − x is divisible by every monic irreducible
# over GF(p) of degree d whenever d | k.  After removing factors of
# degree 1,...,k−1, the gcd isolates factors of degree exactly k.
# ------------------------------------------------------------------
my $_pm_ddf = sub {
    my ($f, $p) = @_;

    my $ONE    = Sidef::Types::Number::Number::ONE;
    my $x_poly = __PACKAGE__->new(1 => $ONE, $p);     # monomial x
    my $w      = $x_poly;
    my @factors;

    for my $k (1 .. CORE::int($f->degree->numify)) {
        $w = $w->powmod($p, $f);                      # w = x^(p^k) mod f

        my $diff = $w->sub($x_poly);
        my $gk   = $diff->monic_gcd($f);
        push @factors, [$k, $gk];

        $f = $f->div($gk);                            # exact division; remove all degree-k factors
        last if $f->is_one;
    }

    # Safety net for non-squarefree or degenerate input: if f is still
    # non-trivial after the loop its remaining irreducible factors all
    # have the same degree equal to deg(f) itself.
    push @factors, [CORE::int($f->degree->numify), $f] unless $f->is_one;

    return @factors;    # list of [degree, product_of_that_degree_irreducibles]
};

# ------------------------------------------------------------------
# Internal helper: Equal Degree Factorization (CZ splitting)
#
# Given a monic squarefree f in GF(p)[x] that is a product of m
# distinct monic irreducibles each of degree d, returns the list of
# those irreducibles.
#
# For odd p, uses the quadratic-residue (Euler-criterion) split:
#   pick random t, compute h = t^((p^d−1)/2) − 1 mod f,
#   then g = gcd(h, f).
# A random t falls on each side of the QR/non-QR split for roughly
# half the irreducible factors of f, giving a non-trivial g with
# probability ≥ 1 − 2^(1−m).
#
# For p = 2, uses the absolute trace map instead:
#   h = t + t^2 + t^(2^2) + ... + t^(2^(d−1)) mod f.
# ------------------------------------------------------------------
my $_pm_edf = sub {
    my ($f, $d, $p) = @_;

    my $deg_f = CORE::int($f->degree->numify);

    return ()   if $deg_f == 0;
    return ($f) if $deg_f == $d;    # already a single irreducible

    my $ONE      = Sidef::Types::Number::Number::ONE;
    my $is_char2 = $p->eq(Sidef::Types::Number::Number::TWO);
    my $d_num    = Sidef::Types::Number::Number::_set_int($d);

    # Exponent (p^d − 1) / 2 — only used for odd p.
    my $exp =
      $is_char2
      ? undef
      : $p->pow($d_num)->dec->idiv(Sidef::Types::Number::Number::TWO);

    my ($g, $h);

    for (1 .. 200) {    # retry up to 200 times (expected O(1) per call)

        # --- Generate a random non-zero polynomial of degree < deg_f
        #     with coefficients drawn uniformly from {0 ... p−1}.
        my %terms;
        for my $k (0 .. $deg_f - 1) {
            my $c = $p->irand;
            $terms{$k} = $c if $c;
        }
        my $t = __PACKAGE__->new(%terms, $p);
        next if $t->is_zero;

        if ($is_char2) {

            # --- Characteristic 2: absolute trace map
            # T(t) = t + t^2 + t^(2^2) + ... + t^(2^(d−1))  mod f
            $h = $t->mod($f);
            my $ti = $h;
            for my $i (1 .. $d - 1) {
                $ti = $ti->powmod($p, $f);     # t^(2^i) mod f
                $h  = $h->add($ti)->mod($f);
            }
        }
        else {
            # --- Odd characteristic: quadratic-residue split
            # h = t^((p^d−1)/2) − 1  mod f
            $h = $t->powmod($exp, $f)->sub(__PACKAGE__->new(0 => $ONE, $p));
        }

        $g = $h->monic_gcd($f);

        # Non-trivial split found
        last if (!$g->is_one && !$g->eq($f));
    }

    # If we exhausted retries without a split (should not happen for
    # valid squarefree input with a prime p), return f as-is.
    return ($f) unless (defined($g) && !$g->is_one && !$g->eq($f));

    # Recurse on both halves.
    my $f_over_g = $f->div($g);
    return (__SUB__->($g, $d, $p), __SUB__->($f_over_g, $d, $p));
};

sub factor_exp {
    my ($original) = @_;

    my $p   = $original->[1];                      # scalar prime (Number)
    my $ONE = Sidef::Types::Number::Number::ONE;

    # -------------------------------------------------------------------
    # Stage 1 – Preprocessing
    # Extract leading coefficient, make monic, then squarefree.
    # -------------------------------------------------------------------
    my $lc = $original->lift->leading_coefficient;    # Number

    my $f = $original->normalize_to_monic;

    # Squarefree part:  f / gcd(f, f′)
    # Any repeated irreducible factor h^k (k ≥ 2) contributes h^(k−1)
    # to the GCD, so dividing once strips exactly one copy of each
    # repeated factor.
    my $df = $f->derivative;
    my $sq = $f->monic_gcd($df);    # = product of (irred ^ (mult−1))
    $f = $f->div($sq);

    # Renormalize to monic (the division may shift the leading coeff
    # slightly due to coefficient arithmetic).
    $f = $f->normalize_to_monic unless $f->is_zero || $f->is_one;

    # -------------------------------------------------------------------
    # Stage 2 – Distinct Degree Factorization
    # @ddf_groups is a list of [degree, poly] pairs.
    # -------------------------------------------------------------------
    my @ddf_groups = $f->is_one ? () : $_pm_ddf->($f, $p);

    # -------------------------------------------------------------------
    # Stage 3 – Equal Degree Factorization (Cantor-Zassenhaus)
    # Collect all monic irreducible factors (without multiplicities yet).
    # -------------------------------------------------------------------
    my @irreducibles;
    for my $pair (@ddf_groups) {
        my ($d, $fk) = @$pair;
        next if $fk->is_one;
        push @irreducibles, $_pm_edf->($fk, $d, $p);
    }

    # -------------------------------------------------------------------
    # Stage 4 – Compute multiplicities in the *original* polynomial.
    # For each monic irreducible h, count the largest e such that
    # h^e divides the original.
    # -------------------------------------------------------------------
    my $remainder = $original;
    my @factors;

    for my $irred (@irreducibles) {
        my $e = 0;
        while (1) {
            my ($q, $r) = $remainder->divmod($irred);
            last unless $r->is_zero;
            $e++;
            $remainder = $q;
        }
        ($e > 0) or next;
        push @factors, Sidef::Types::Array::Array->new([$irred, Sidef::Types::Number::Number::_set_int($e)]);
    }

    Sidef::Types::Array::Array->new(\@factors)->sort->unshift(Sidef::Types::Array::Array->new([$lc, $ONE]));
}

sub factor {
    my ($self) = @_;
    Sidef::Types::Array::Array->new([map { ($_->[0]) x CORE::int($_->[1]) } @{$self->factor_exp}]);
}

{
    no strict 'refs';

    foreach my $method (qw(ge gt lt le)) {
        *{__PACKAGE__ . '::' . $method} = sub {
            my ($x, $y) = @_;
            ($x->cmp($y) // return undef)->$method(Sidef::Types::Number::Number::ZERO);
        };
    }

    *{__PACKAGE__ . '::' . '/'}   = \&div;
    *{__PACKAGE__ . '::' . '//'}  = \&idiv;
    *{__PACKAGE__ . '::' . '÷'}   = \&div;
    *{__PACKAGE__ . '::' . '*'}   = \&mul;
    *{__PACKAGE__ . '::' . '%'}   = \&mod;
    *{__PACKAGE__ . '::' . '+'}   = \&add;
    *{__PACKAGE__ . '::' . '-'}   = \&sub;
    *{__PACKAGE__ . '::' . '**'}  = \&pow;
    *{__PACKAGE__ . '::' . '++'}  = \&inc;
    *{__PACKAGE__ . '::' . '--'}  = \&dec;
    *{__PACKAGE__ . '::' . '<'}   = \&lt;
    *{__PACKAGE__ . '::' . '>'}   = \&gt;
    *{__PACKAGE__ . '::' . '<=>'} = \&cmp;
    *{__PACKAGE__ . '::' . '<='}  = \&le;
    *{__PACKAGE__ . '::' . '≤'}   = \&le;
    *{__PACKAGE__ . '::' . '>='}  = \&ge;
    *{__PACKAGE__ . '::' . '≥'}   = \&ge;
    *{__PACKAGE__ . '::' . '=='}  = \&eq;
    *{__PACKAGE__ . '::' . '!='}  = \&ne;
}

1
