#!/usr/bin/env python3

from Cryptotools.Numbers.primeNumber import gcd, isPrimeNumber
from Cryptotools.lcm import lcm
from Cryptotools.Numbers.coprime import phi
from Cryptotools.Utils.utils import gcd
from sympy import factorint
from functools import reduce


def carmichael_lambda(n: int) -> int:
    """
        This function is a carmichael lambda for identifying the smallest integer m
        and saisfy this condition: a ** m congrue 1 modulo n
        This function is relatively closed with the euler's totient (phi of n)

        Args:
            n (Integer): found the smallest integer of n

        Returns:
            REturn the smallest integer of n        
    """

    # First, factorizing n number
    # factorint return a list of prime factors of n
    # Base on that theorem:
    #    https://en.wikipedia.org/wiki/Fundamental_theorem_of_arithmetic
    factorsN = factorint(n)

    """
        The result of the factorint, we have the following:
        n = p ** e1, p ** e2, ..., p ** ek
    """

    factors = list()

    for p, e in factorsN.items():
        # Base on the theorem of arithmetic, p is always prime
        if isPrimeNumber(p):
            if p == 2 and e >= 3:
                factors.append(phi(pow(p, e)) // 2)
            elif p >= 2 and e > 0:
                factors.append(phi(pow(p, e)))
            elif p > 2:
                factors.append(phi(pow(p, e)))

    # Now, we can compute lcm 
    result = reduce(lcm, factors)

    return result

def carmichael_lambda2(n):
    """
        Deprecated function
    """
    # Get all coprimes with n
    coprimes = list()
    for a in range(1, n):
        if gcd(a, n) == 1:
            coprimes.append(a)

            # Need to find the smallest value
            """
                Need to find the smallest value m. Must to satisfy this condition:
                a ** m congrus 1 mod n
            """
            for m in range(1, n):
                print(f"{m} {a ** m % n}")

    print(coprimes)

def carmi_numbers(n):
    """
        This function get all GCD of the number if these number are prime or not
        
        Args:
            n (Integer): Iterate to n elements

        Returns:
            Return a list of prime numbers
    """
    # https://kconrad.math.uconn.edu/blurbs/ugradnumthy/carmichaelkorselt.pdf
    primes = list()
    for i in range(2, n - 1):
        if gcd(n, i) == i:
            if isPrimeNumber(i):
                primes.append(i)

    return primes

def is_carmichael(n, primes):
    """
        This function check if the n number is a carmichael number. The arguments primes at least 3 prime numbers
        As it's said in the Carmichael theorem, a carmichael number has three factors and they are all primes: https://en.wikipedia.org/wiki/Carmichael_number
        With the Korselt's criterion, to check if the number is a carmichael number p - 1 / n - 1.

        Args:
            n (Integer): the carmichael number
            primes (list): list of prime numbers

        Returns:
            Boolean of the carmichael's number result
    """
    r = True
    n = n - 1

    if len(primes) < 3:
        return False

    # Korselt's criterion
    for p in primes:
        p = p - 1
        if n % p != 0:
            r = False
            break
    return r

def is_carmi_number(n) -> bool:
    """
        This function check if the number n is a carmichael number (pseudoprime)

        Args:
            n (Integer): Iterate to n elements

        Returns:
            Return a Boolean if n is a carmichael number or not. True if yes
    """
    j = 0
    for i in range(n):
        if i ** n % n == i:
            j += 1
    if j == n:
        return True
    return False

def generate_carmi_numbers(nbr) -> list:
    """
        This function generate carmichael numbers, they are called pseudoprimes
        For instance: a = 545, n = 561
        gcd(a, n) = 1
        pow(a, n - 1) % n = 1

        Args:
            nbr (Integer): Find all pseudoprimes until nbr is achieves

        Returns:
            Return the list of pseudoprimes
    """
    carmi = list()

    count = 0
    n = 2
    while count < nbr:
        # First, we need to find a, coprime with n
        for a in range(1, n):
            # We check if n is coprime with a
            # All integers must be coprime with n, a belong to n
            if gcd(a, n) == 1:
                # Check if is a carmichael number
                # Fermat's little theorem, a ** (n - 1) % n, must be congruent to 1
                if pow(a, (n - 1)) % n == 1:
                #if (a ** (n - 1)) % n == 1:
                    #print(f"{gcd(a, n)} {a} {n}")
                    if n not in carmi:
                        count += 1
                        carmi.append(n)
            else:
                print(f"{gcd(a, n)} {a} {n}")

                break
        n += 1
    print(carmi)
    #for cop in range(2, n):
    #    if gcd(cop, n) == 1:
    #        coprimes.append(cop)

    return carmi
