use v6-alpha;

# This function multiplies the squaring factors of $n and $m to receive
# the squaring factors of ($n*$m)
sub multiply_squaring_factors($n,$m)
{
    return sort { $^a <=> $^b }, one(@$n,@$m).values;
}

my %gsf_cache = (1 => []);

# This function gets the squaring factors of $n.
# The squaring factors are those prime numbers that need to be multiplied
# by $n to reach a perfect square. They are the minimal such number.

sub get_squaring_factors($n,$start_from=2)
{
    if (%gsf_cache.exists($n))
    {
        return %gsf_cache{$n};
    }

    my $p = [//] grep { $n % $_ == 0 }, ($start_from .. $n);
    # This function is recursive to make better use of the Memoization
    # feature.
    return 
        (%gsf_cache{$n} = 
            multiply_squaring_factors(
                [$p], 
                get_squaring_factors(int($n / $p), $p)
            )
        );
}


sub Graham($n)
{
    my $n_sq_factors = get_squaring_factors($n);
    
    # The graham number of a perfect square is itself.
    if ($n_sq_factors.elems() == 0)
    {
        return ($n, [$n]);
    }

    # Cheating: 
    # Check if between n and n+largest_factor we can fit
    # a square of SqFact{n*(n+largest_factor)}. If so, return
    # n+largest_factor.
    #
    # So, for instance, if n = p than n+largest_factor = 2p
    # and so SqFact{p*(2p)} = 2 and it is possible to see if
    # there's a 2*i^2 between p and 2p. That way, p*2*i^2*2p is
    # a square number.

    {
        my $largest_factor = $n_sq_factors.[-1];

        my ($lower_bound, $lb_sq_factors);
        
        $lower_bound = $n + $largest_factor;
        loop
        {
            $lb_sq_factors = get_squaring_factors($lower_bound);
            if (grep { $_ == $largest_factor }, @$lb_sq_factors)
            {
                last;
            }
            $lower_bound += $largest_factor;
        }

        my $n_times_lb = 
            multiply_squaring_factors($n_sq_factors, $lb_sq_factors);

        my $rest_of_factors_product = [*] @$n_times_lb;

        my $low_square_val = int(sqrt($n/$rest_of_factors_product));
        my $high_square_val = int(sqrt($lower_bound/$rest_of_factors_product));
        
        if ($low_square_val != $high_square_val)
        {
            return ($lower_bound, [$n, ($low_square_val+1)*($low_square_val+1)*$rest_of_factors_product,$lower_bound]);
        }
    }

    # %primes_to_ids_map maps each prime number to its ID. IDs are consective.
    my (%primes_to_ids_map);
    my $next_id = 0;

    # @base is an array that for each ID of a prime number holds the 
    # controlling vector for this number.
    #
    # This is in fact a matrix that is kept stair-shaped and canonized.
    my (@base);

    # The integers whose multiplication compose this vector
    my (@base_composition);

    # Register all the primes in the squaring factors of $n
    for @$n_sq_factors -> $p
    {
        %primes_to_ids_map{$p} = ($next_id++);
    }

    # $n_vec is used to determine if $n can be composed out of the base's 
    # vectors.
    my $n_vec = $n_sq_factors;

    my $n_composition = [ $n ];

    my $i;

    loop ($i = $n+1 ; ; $i++)
    {
        my $i_sq_factors = get_squaring_factors($i);
        
        # Skip perfect squares - they do not add to the solution
        if ($i_sq_factors.elems() == 0)
        {
            next;
        }

        # Check if $i is a prime number
        # We need n > 2 because for n == 2 it does include a prime number.
        #
        # Prime numbers cannot be included because 2*n is an upper bound
        # to G(n) and so if there is a prime p > n than its next mulitple
        # will be greater than G(n).
        if (($n > 2) && ($i_sq_factors[0] == $i))
        {
            next;
        }

        # $final_vec is the new vector to add after it was
        # stair-shaped by all the controlling vectors in the base.

        my $final_vec = $i_sq_factors;

        my $final_composition = [ $i ];

        for @$i_sq_factors -> $p
        {
            if (!%primes_to_ids_map.exists($p))
            {
                # Register a new prime number
                %primes_to_ids_map{$p} = ($next_id++);
            }
            else
            {
                my $id = %primes_to_ids_map{$p};
                if (defined(@base[$id]))
                {
                    $final_vec = multiply_squaring_factors($final_vec, @base[$id]);
                    $final_composition = multiply_squaring_factors($final_composition, @base_composition[$id]);
                }
            }
        }
 
        # Get the minimal ID and its corresponding prime number
        # in $final_vec.
        my $min_id = -1;
        my $min_p = 0;

        for @$final_vec -> $p
        {
            my $id = %primes_to_ids_map{$p};
            if (($min_id < 0) || ($min_id > $id))
            { 
                $min_id = $id;
                $min_p = $p;
            }
        }

        if ($min_id >= 0)
        {
            # Assign $final_vec as the controlling vector for this prime
            # number
            @base[$min_id] = $final_vec;
            @base_composition[$min_id] = $final_composition;
            # Canonize the rest of the vectors with the new vector.
            my $j;
            loop ($j=0 ; $j < @base.elems() ; $j++)
            {
                if (($j == $min_id) || (!defined(@base[$j])))
                {
                    next;
                }
                if (grep { $_ == $min_p }, @base[$j][])
                {
                    @base[$j] = multiply_squaring_factors(@base[$j], $final_vec);
                    @base_composition[$j] = multiply_squaring_factors(@base_composition[$j], $final_composition);
                }
            }
        }
 
        # A closure to print the base. It is not used but can prove useful.
        my $print_base = sub {
            print "Base=\n\n";
            my $j;
            loop ($j=0 ; $j < @base.elems() ; $j++)
            {
                next if (! defined(@base[$j]));
                print "base[$j] (" ~ join(" * ", @base[$j][]) ~ ")\n";
            }
            print "\n\n";
        };

        # Check if we can form $n

        while ($n_vec.elems())
        {
            # Assing $id as the minimal ID of the squaring factors of $p
            my @ids_vec = (sort { $^a <=> $^b }, %primes_to_ids_map{@$n_vec});
            my $id = @ids_vec[0];
            # Mulitply by the controlling vector of this ID if such one exists
            # or terminate if there isn't such.
            last if (!defined(@base[$id]));
            $n_vec = multiply_squaring_factors($n_vec, @base[$id]);
            $n_composition = multiply_squaring_factors($n_composition, @base_composition[$id]);
        }
        if ($n_vec.elems() == 0)
        {
            return ($i, $n_composition);
        }
    }
}

my $start = int(@*ARGS.shift);
my $end = int(@*ARGS.shift || $start);
my $n;
for $start..$end -> $n
{
    my ($g_val, $composition) = Graham($n);
    say "G($n) = $g_val [{join(",", @$composition)}]";
}



