from functools import lru_cache


def get_digits_reversed(n):
    """
    Returns a list of digits for n.
    """
    digits, rest = [], n
    while rest != 0:
        digits.append(rest % 10)
        rest //= 10
    return digits


def is_palindrome_string(n):
    """
    Checks whether n is a palindrome number
    with a string based algorithm.

    :param n: number to check
    :returns: boolean

    """
    s = str(n)
    return s == s[::-1]


def is_palindrome_integer(n):
    """
    Checks whether n is a palindrome number
    with an integer based algorithm.

    :param n: number to check
    :returns: boolean
    """
    digits = get_digits_reversed(n)
    digits_count = len(digits)
    for i in range(digits_count // 2):
        if digits[i] != digits[digits_count - i - 1]:
            return False
    return True


def time_is_palindrom():
    """ I just want to check which option is faster. """
    from timeit import timeit
    print(timeit(lambda: is_palindrome_integer(82445254428), number=100000))
    print(timeit(lambda: is_palindrome_string(82445254428), number=100000))
    # > 0.42330633100027626
    # > 0.07605635199979588


is_palindrome = is_palindrome_string


def get_item_counts(l):
    """
    Counts how often each element is part of the list.

    :param l: a list
    :returns: a dictionary
    """
    d = {}
    for e in l:
        try:
            d[e] += 1
        except KeyError:
            d[e] = 1
    return d


def product(l):
    """
    Calculates the product of all items in the list.

    :param l: a list
    :returns: a number that is the product of all elements in the list
    """
    from functools import reduce
    import operator
    return reduce(operator.mul, l, 1)


def triangle_numbers():
    c = 0
    i = 1
    while True:
        c += i
        yield c
        i += 1


def even(n):
    """
    Returns true if a number is even.
    """
    return n % 2 == 0


def odd(n):
    """
    Returns true if a number is odd.
    """
    return n % 2 != 0


def collatz_sequence(n):
    """
    Returns collatz sequence for n.

    :param n: collatz sequence
    """
    cs = []
    while n != 1:
        cs.append(n)
        n = n // 2 if n % 2 == 0 else 3 * n + 1
    cs.append(n)
    return cs


@lru_cache(maxsize=1000000)
def collatz_sequence_length(n):
    """
    Returns length of collatz sequence for n.

    :param n: collatz sequence
    """
    if n == 1:
        return 1
    length = 1
    while odd(n):
        n = 3 * n + 1
        length += 1
    return length + collatz_sequence_length(n // 2)


@lru_cache(maxsize=10000)
def factorial(n):
    p = 1
    for i in range(1, n + 1):
        p *= i
    return p


def proper_divisors(n):
    """
    Returns the list of divisors for n excluding n.
    """
    if n < 2:
        return []
    divisors = [1, ]
    d = 2
    while d * d <= n:
        if n % d == 0:
            divisors.append(d)
        d += 1
    # Ignore first element and iterate list backwards.
    for d in divisors[1:][::-1]:
        q = n // d
        if q != d:
            divisors.append(q)
    return divisors


def sum_proper_divisors(n):
    """
    Returns the sum of proper divisors of a number.
    """
    if n < 2:
        return 0
    s = 1
    d = 2
    while d * d <= n:
        if n % d == 0:
            s += d
            q = n // d
            if q != d:
                s += q
        d += 1
    return s


def permutations(iterable):
    """
    Generator that returns all permutations for the iterable.

    Generates equivalent result to itertools.permutations.
    """
    if not iterable:
        yield iterable
    for i in range(len(iterable)):
        elem = iterable[i:i + 1]
        rest = iterable[:i] + iterable[i + 1:]
        for ps in permutations(rest):
            yield elem + ps


def gcd(a, b):
    """ Returns the greatest commond divisor of a and b. """
    if b == 0:
        return a
    return gcd(b, a % b)
