%load_ext autoreload
%autoreload 2

from common.utils import prime_factors

from itertools import count, tee
from six.moves import map, reduce, zip

"""
Explanation: The first two consecutive numbers to have two distinct prime factors are:
$$
14 = 2 × 7 \
15 = 3 × 5
$$
The first three consecutive numbers to have three distinct prime factors are:
$$
644 = 2² × 7 × 23 \
645 = 3 × 5 × 43 \
646 = 2 × 17 × 19.
$$
Find the first four consecutive integers to have four distinct prime factors. What is the first of these numbers?
Version 1: Fun times with Itertools
First let's load up the prime factorization function we implemented earlier. Recall that this function returns a Counter with the prime factor as the key, and its exponent as the value.
End of explanation
"""


def nwise(iterable, n=2):
    iters = tee(iterable, n)
    for i, it in enumerate(iters):
        for _ in range(i):
            next(it, None)
    return zip(*iters)

"""
Explanation: Define an $n$-wise iterator over an iterator, inspired by the implementation of pairwise in the Itertools recipes. It get $n$ iterators for the iterable, advances the $i$th iterator by $i$, and zips them back together.
End of explanation
"""


list(nwise(range(10), 9))

list(nwise(range(10), 4))

"""
Explanation: <!-- TEASER_END -->

See some example calls below.
End of explanation
"""


def consecutive_distinct_factors(n, m):
    """
    The first consecutive n numbers 
    to have m distinct prime factors
    """
    for factors in nwise(map(prime_factors, count(2)), n):
        if all(map(lambda x: len(x) == m, factors)):
            return factors

consecutive_distinct_factors(2, 2)

"""
Explanation: Now we can define our function, which (lazily) evaluates the prime factors of all integers greater than 1 and iterates over them $n$-wise (can be thought of as a sliding window of size $n$.) We use len to count the number of distinct factors (since the factors are returned as a Counter) and return the window if the numer of distinct prime factors of all numbers in the window is equal to $m$.
End of explanation
"""


# recall Python's built-in pow function
pow(3, 3)

c = prime_factors(24); c

# recall that map can be applied 
# to a variable number of lists, 
# i.e. map(func, (a0, a1, ...), (b0, b1, ...)) -> [func(a0, b0), func(a1, b1), ...]
list(map(pow, c.keys(), c.values()))

"""
Explanation: We could modify the loop in the method to return the first number from the window, rather than the prime factors of all the numbers in the window, but this makes our implementation unecessarily long and messy. Also, the prime factorizations actually yield more information, and while we could return both, we can also just uniquely determine the multiple from the prime factors.
End of explanation
"""


prod = lambda xs: reduce(lambda x, y: x*y, xs, 1)

"""
Explanation: Define the prod function, which is analogous to Python's built-in sum function.
End of explanation
"""


prod([])

# calculate 6!
prod(range(1, 6+1))

prod(map(pow, c.keys(), c.values()))

multiple = lambda factors: prod(map(pow, factors.keys(), factors.values()))

"""
Explanation: By convention, $x^0 = 1$ for all $x$ and $0! = 1$
End of explanation
"""


multiple(c)

"""
Explanation: Evaluate $2^3 \cdot 3$
End of explanation
"""


list(map(multiple, consecutive_distinct_factors(2, 2)))

list(map(multiple, consecutive_distinct_factors(3, 3)))

list(map(multiple, consecutive_distinct_factors(4, 4)))

"""
Explanation: Now we can get the actual number, rather than the prime factorization.
End of explanation
"""


