import copy
import string
import time


############################################################
# list mutation
############################################################


def get_primes():
    return [2, 3, 5, 7, 11, 13, 17]


def direct_index_assignment(numbers):
    for i in range(len(numbers)):
        if i % 2 == 0:
            numbers[i] *= 100
    print(numbers)


# direct_index_assignment(get_primes())


def grow_shrink_by_element(numbers):
    numbers.append(min(numbers))
    numbers.append(max(numbers))
    print(numbers)

    numbers.extend(get_primes()[:3])
    print(numbers)

    # while 2 in numbers:
    #     numbers.remove(2)
    #     print("  removing 2")
    # print(numbers)
    # numbers.remove(7)
    # # numbers.remove(7)  # error
    # print(numbers)

    ################

    # print(id(numbers))

    # numbers += ["a", "b", "c"]  # equivalent to .extend()
    # print(numbers)
    # print(id(numbers))

    # numbers.remove("a")  # undo extend()
    # numbers.remove("b")
    # numbers.remove("c")

    # numbers = numbers + ["a", "b", "c"]  # does not mutate
    # print(numbers)
    # print(id(numbers))


# grow_shrink_by_element(get_primes())


def grow_shrink_by_index(numbers):
    print(numbers)

    numbers.insert(0, 100)
    numbers.insert(5, 100)
    numbers.insert(-1, 100)
    print(numbers)

    # val = numbers.pop(-1)
    # print(numbers, val)
    # del numbers[1]
    # print(numbers)


# grow_shrink_by_index(get_primes())


def sort_reverse(numbers):
    print(numbers)
    print(id(numbers))

    result = sorted(numbers)
    print(result)
    print(id(result))

    numbers.sort()
    print(numbers)
    print(id(numbers))

    ################

    # result = reversed(numbers)
    # print(result)
    # print(id(result))
    # for elt in result:
    #     print(elt)
    # print(list(result))

    # numbers.reverse()
    # print(numbers)
    # print(id(numbers))


def transform(numbers):
    results = []
    for num in numbers:
        digits = f"{num:02d}"
        results.append(int(digits[::-1]))
    return results


# sort_reverse(transform(get_primes()))


############################################################
# aliasing and copying
############################################################


def alias_vs_copy():
    mid = [3, 5]
    last = [11]
    end = [9, last]
    all = [1, mid, 7, end]
    print(all)

    # print(id(mid))
    # print(id(all[1]))
    # mid.append(6)
    # print(mid)
    # print(all)
    # all[1].pop()
    # print(mid)
    # print(all)

    ################

    # fibo = mid.copy()
    # # fibo = list(mid)    # equivalent
    # # fibo = mid[:]       # equivalent
    # print(id(mid))
    # print(id(fibo))
    # for _ in range(3):
    #     fibo.append(fibo[-2] + fibo[-1])
    # print(fibo)
    # print(mid)
    # print(all)

    ################

    # all_again = all.copy()
    # print(all_again)
    # all[1][0] = 4
    # print(all_again)
    # all[1][0] = 3  # undo

    # all_over = copy.deepcopy(all)
    # print(all_over)
    # all[1][0] = 4
    # print(all_over)


# alias_vs_copy()


def greet(last_name, profs):
    # print(id(profs))
    alphabet = sorted(string.ascii_lowercase)
    section = profs[alphabet.index(last_name[0])]
    for person in section:
        if person[0].lower() == last_name.lower():
            words = person
            words.reverse()
            words.insert(0, "Prof.")
            words.insert(0, "Hello,")
            print(" ".join(words))


def list_all_names(personnel):
    # print(id(personnel))
    for section in personnel:
        for name in section:
            print(f"{name[0]}, {name[1]}")


def demo_parameter_aliasing():
    a_names = [
        ["Abelson", "Hal"],
        ["Adalsteinsson", "Elfar"],
        ["Adib", "Fadel"],
    ]
    b_names = [
        ["Bahai", "Ahmad"],
        ["Balakrishnan", "Hari"],
        ["Baldo", "Marc"],
    ]
    c_names = [
        ["Carbin", "Michael"],
        ["Chan", "Vincent"],
        ["Chandrakasan", "Anantha"],
    ]
    faculty = [a_names, b_names, c_names]
    # print(id(faculty))
    greet("balakrishnan", faculty)
    list_all_names(faculty)


# demo_parameter_aliasing()


############################################################
# mutation examples
############################################################


def keep_unique(numbers):
    """
    Mutate a list of numbers to contain only one instance of each of its
    elements, and sorted in decreasing order.
    """
    seen = []
    for num in numbers:
        if num not in seen:
            seen.append(num)
    seen.sort()
    return seen  # TODO: mutate numbers instead


def test_keep_unique():
    numbers = [5, 1, 5, 1, 1, 1, 5, 3, 1, 1, 5, 2, 1, 1, 3, 1, 5]
    keep_unique(numbers)
    print(numbers)


# test_keep_unique()


########################################


def delete_at(items, indices):
    """Given a list, delete the elements at the indices specified."""
    # approach 1: delete from beginning
    # TODO: fix this
    for i in sorted(indices):
        del items[i]

    # approach 2: delete from end


def test_delete_at():
    sequence = list(range(10))
    delete_at(sequence, [2, 7, 5, 4])
    print(sequence)


# test_delete_at()


def keep_unique(numbers):
    # alternate implementation that doesn't clear() the original list
    indices_to_remove = []
    for i in range(len(numbers)):
        if numbers[i] in numbers[:i]:
            indices_to_remove.append(i)
    delete_at(numbers, indices_to_remove)
    numbers.sort()


# test_keep_unique()


########################################


def remove_largest(numbers, k):
    """
    Remove from a list of numbers its largest k elements. The original
    ordering of the remaining elements does not have to be preserved.
    """
    # TODO: exercise


def test_remove_largest():
    numbers = list(range(10))
    numbers += numbers
    print(numbers)

    remove_largest(numbers, k=4)
    print(numbers)
    remove_largest(numbers, k=3)
    print(numbers)


# test_remove_largest()


############################################################
# mutating while looping
############################################################


def square_in_place(numbers):
    """
    Mutate a list of numbers so that each element is replaced with its square.
    """
    # TODO: fix this
    for num in numbers:
        num = num ** 2


def test_square_in_place():
    numbers = list(range(10, 16))
    print(numbers)
    square_in_place(numbers)
    print(numbers)


# test_square_in_place()


########################################


def extend_sequence(seq):
    """
    Given a list of consecutive ints, double its length while continuing
    the sequence.
    """
    # append() over individual append elements, incorrect
    for num in seq:
        seq.append(num + len(seq))

    # solution: loop over some sequence other than what you're mutating

    # one approach: loop over range() of indicies
    # num_elts = len(seq)
    # for i in range(num_elts):
    #     seq.append(seq[i] + num_elts)

    # another approach: loop over a copy of seq
    # num_elts = len(seq)
    # original = seq.copy()
    # for elt in original:
    #     seq.append(elt + num_elts)

    # or, operate on sequences directly: use extend()
    # num_elts = len(seq)
    # seq.extend(list(range(seq[0] + num_elts, seq[0] + num_elts * 2)))


def test_extend_sequence():
    numbers = list(range(1, 6))
    print(numbers)
    extend_sequence(numbers)
    print(numbers)


# test_extend_sequence()


########################################


def remove_elements(items, to_remove):
    """
    Given a list of items, remove any of its elements that appear in the
    list to_remove, keeping the remaining elements in the same order.
    """
    # incorrect: as items shrinks, indices become offset
    original = items.copy()
    for i in range(len(original)):
        if original[i] in to_remove:
            del items[i]

    # approach 1: iterate over to_remove
    # for target in to_remove:
    #     while target in items:
    #         items.remove(target)
    #     # alteratively, less expensive than repeatedly evaluating `target in items`
    #     # for _ in range(items.count(target)):
    #     #     items.remove(target)

    # approach 2: work backwards in items
    # for i in range(len(items) - 1, -1, -1):
    #     if items[i] in to_remove:
    #         del items[i]

    # approach 3: clear() and extend()
    # result = []
    # for elt in items:
    #     if elt not in to_remove:
    #         result.append(elt)
    # items.clear()
    # items.extend(result)


def test_remove_elements():
    numbers = [4, 3, 6, 7, 3, 2, 1, 3]
    begone = [1, 3]
    print(numbers)
    print(begone)
    remove_elements(numbers, begone)
    print(numbers)


# test_remove_elements()
