from functools import lru_cache
try:
    from lib_misc import get_item_counts
    from lib_misc import product
except ModuleNotFoundError:
    from .lib_misc import get_item_counts
    from .lib_misc import product


def prime_factors(n):
    """
    Returns a list of prime factors for n.

    :param n: number for which prime factors should be returned
    """
    # TODO: Look into using a prime wheel instead.
    factors = []
    rest = n
    divisor = 2
    while rest % divisor == 0:
        factors.append(divisor)
        rest //= divisor
    divisor = 3
    while divisor * divisor <= rest:
        while rest % divisor == 0:
            factors.append(divisor)
            rest //= divisor
        divisor += 2
    if rest != 1:
        factors.append(rest)
    return factors


def prime_factors_count(n):
    """
    Returns a dictionay of primes where each key is a prime
    and the value how many times that prime is part of the factor of n.

    :param n: numober for which prime factor counts are returned
    :returns: a dict where they key is a prime and the value
              the count of how often that value occurs

    """
    return get_item_counts(prime_factors(n))


@lru_cache(maxsize=10000)
def is_prime(n):
    """Returns True if n is prime and False otherwise.

    :param n: number to be checked

    """
    if n < 2:
        return False
    if n == 2 or n == 3:
        return True
    if n % 2 == 0:
        return False
    d = 3
    while d * d <= n:
        if n % d == 0:
            return False
        d += 2
    return True


def prime_nth(n):
    """Returns the nth prime number. The first number
    is 1 indexed, i.e. n = 1 -> 2, n = 2 -> 3, etc.

    :param n:

    """
    if n == 1:
        return 2
    if n == 2:
        return 3
    p = 5
    i = 3
    while i < n:
        p = p + 2
        if is_prime(p):
            i += 1
    return p


def primes(n_max):
    """
    Returns a list of all primes smaller n based
    on sieve of erasthones algorithm.

    If bitarray module is installed it will be used
    for the array, otherwise a regular Python list is
    used. The function should work for much larger
    numbers with bitarray.

    :param n_max:
    """
    try:
        import bitarray
        b = bitarray.bitarray(n_max)
        b.setall(True)
    except ModuleNotFoundError:
        b = [True for _ in range(n_max)]
    n = 1
    b[n - 1] = False
    while n * n <= n_max:
        if b[n - 1] is True:
            for i in range(n + n, n_max + 1, n):
                b[i - 1] = False
        n += 1
    ps = []
    for i in range(1, n_max + 1):
        if b[i - 1]:
            ps.append(i)
    return ps


def get_divisors_count(n):
    """
    Returns the number of divisors for n.
    The numbers 1 and n count as a divisor.

    >>> get_divisors_count(1)
    1
    >>> get_divisors_count(3)
    2 # 1, 3
    >>> get_divisors_count(4)
    3 # 1, 2, 4

    Getting the number of divisors is a combinatorial
    problem that can be solved by using the counts
    for each prime factor. For example, consider

    2 * 2 * 7 = 28

    We have 3 options for 2 (1, 1 * 2, 2 * 2)
    and 2 options for 7 (1, 1 * 7).

    By multiplying those options we get the number
    of combinations:

    2 * 3 = 6
    """
    if n == 1:
        return 1
    factors = prime_factors_count(n)
    count = product([v + 1 for v in factors.values()])
    return count
