############################################################
# list objects, basic operations
############################################################


# So far, we've seen the following object types in Python:
#
#   int, float, bool, str, range
#
# The first three represent individual values; they cannot be broken
# down further. The other two represent sequences of data, but they are
# restricted to a particular form: characters for strs, and regularly
# spaced ints for ranges.
#
# Often, it is convenient to store a sequence of values without any
# restriction. To begin with, we may not know in advance how many values
# there are. The following function demonstrates collecting input strs
# from the terminal into a **list** object `names`. Uncomment the line
# below that calls collect_names(), and try it out.


def collect_names():
    print('Enter a sequence of names, or "STOP" to finish.')
    print()

    # create an empty list
    names = []
    while True:

        # collect a str input and add it to the list
        entry = input("    Add a name: ")
        if entry == "STOP":
            break
        names.append(entry)
        print(f"    {names = }")
        print()

    # print each name on its own line by iterating through the list
    for entry in names:
        print(f"    {entry}")
    print()


# print()
# collect_names()


# What this demonstrates is the possibility of storing an arbitrary
# amount of data in one object over the course of the program. Note the
# pattern of writing a potentially infinite loop with `while True:`, but
# providing a way to `break` out of it.
#
# The following functions in this file demonstrate the basic mechanics
# of working with list objects. Uncomment the appropriate lines to run
# each function, and study the syntax and meaning of each operation.
# We'll review at the beginning of class and then move on to discuss
# further consequences.


def demo_list_creation():
    # list object syntax:
    # comma-separated values, surrounded by square brackets
    odds = [1, 3, 5, 7, 9, 11]
    print(odds)

    # convert from range() to list
    odds_range = range(1, 12, 2)
    also_odds = list(odds_range)
    print(odds_range)
    print(also_odds)
    print(odds == also_odds)  # comparison checks each element
    print(odds == list(range(1, 14, 2)))

    # cannot convert from list to range, why?
    evens = [2, 4, 6, 8]
    # evens_range = range(evens)  # this line results in an error

    # elements of a list can be objects of any type, even other lists
    nested_odds = [1, [3, 5], "seven", [9, [11]]]
    print(nested_odds)
    a = [3, 5]
    b = [11]
    c = [9, b]
    equivalent_nesting = [1, a, "seven", c]
    print(equivalent_nesting)
    print(nested_odds == equivalent_nesting)


# print()
# demo_list_creation()


############################################################
# list operations and strs
############################################################


def demo_list_indices():
    dozen = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
    print(f"{dozen = }")

    # indexing
    for i in range(2, 7):
        print(f"dozen[{i}] = {dozen[i]}")
    print()

    # slicing
    odds = dozen[::2]
    evens = dozen[1::2]
    print(f"{odds = }")
    print(f"{evens = }")
    print()


# print()
# demo_list_indices()


def demo_list_operations():
    numbers = [1, 2, 3]
    words = ["one", "two", "three"]

    # concatenation, repetition
    print(numbers + words)
    print((numbers + words) * 4)
    print()

    # iteration, len()
    together = numbers + words
    for elt in together:
        print(elt)
    for i in range(len(together)):
        print(together[i])
    print()

    # comparisons: == and <
    print(numbers == [1, 2, 3])
    print(numbers < [1, 3, 1])
    print()

    # min() and max() use < operator internally
    print(min(numbers), max(numbers))
    print(min(words), max(words))
    # print(min(together), max(together))  # why doesn't this work?
    print()


# print()
# demo_list_operations()


def demo_list_membership():
    # recall string membership test `in`?
    # matches any substring, including length-1 strs
    title = "alice in wonderland"
    print("a" in title)
    print("l" in title)
    print("alice" in title)
    print(" in wonder" in title)
    print()

    # list membership test is by element-only, not subsequences
    numbers = [1, 2, 3]
    words = ["one", "two", "three"]
    together = numbers + words
    print(3 in together)
    print("one" in together)
    print([3, "one"] in together)
    print()

    # list membership test uses ==, needs exact match
    print("three" in words)
    print("thre" in words)
    print()

    # == comparison still applies when testing for lists in lists
    pairs = []
    for i in range(3):
        pairs.append( [numbers[i], words[i]] )
    print(pairs)
    print([1, "one"] in pairs)
    print(["one", 1] in pairs)
    print()


# print()
# demo_list_membership()


############################################################
# editing lists, i.e., mutation
############################################################


# A key difference between lists and other object types we've seen is
# that the contents of list objects can be modified. This is called
# **mutation**, and it has important consequences for the meaning of
# programs, particularly when interacting with functions. We'll discuss
# this more in class. For now, the function below demonstrates some
# basic list mutation operations.
#
# Remember: strs, ints, floats, etc. are all immutable, whereas lists
# are the only mutable type we've seen.


def print_state(subjects, instructors):
    print(subjects)
    print(instructors)
    print()


def demo_list_mutation():
    subjects = ["6.100", "6.100A"]
    instructors = ["Andrew", "Ana"]
    print_state(subjects, instructors)

    # assignment at index
    instructors[0] = "Mickey Mouse"
    print_state(subjects, instructors)

    # list.append() adds a single element at end of list
    subjects.append("6.100B")
    instructors.append("John")
    print_state(subjects, instructors)

    # list.extend() adds multiple elements via a single list object
    instructors[0] = [instructors[0]]
    instructors[0].extend(["Donald Duck", "Goofy"])
    print_state(subjects, instructors)

    # deletion at index
    del subjects[0]
    del instructors[0]
    print_state(subjects, instructors)


# print()
# demo_list_mutation()
