import time


############################################################
# overview
############################################################


# Last Wednesday's lecture concluded with idea of breadth-first search
# (BFS) on directed graphs, which was to explore successive frontiers
# relative to a start node. So far, we haven't had any other algorithm
# to compare against, besides a very simple random walk, so the notion
# of "breadth-first" may not be super-apparent yet.

# In tomorrow's lecture, we will explore another strategy called depth-
# first search (DFS), which is sounds conceptually similar, but is
# fundamentally a very different idea. Hence, our initial implementation
# of it will be quite different from how we implemented BFS, and it will
# involve a technique called **recursion.**

# At face value, recursion in programming is simply having a function's
# body code call the function itself. This may sound like something
# special, but to Python (and to most languages), no special handling is
# required -- the exact same function call mechanism applies.

# However, the idea of recursion fundamentally changes the way we as
# programmers view the structure of problems and their solutions. It is
# a powerful and pervasive theme in computer science, and we will
# present DFS as a prime example of recursive thinking.

# This pre-lecture code introduces recursion on a couple simple
# examples, so you will be better prepared to apply it towards
# understanding and implementing DFS. Recursive code has certain
# implications for the stack of function call frames, so this is also an
# excellent opportunity to practice drawing environment diagrams (and
# check yourself with pythontutor.com).


############################################################
# countdown timer
############################################################


# In recent years, there has been a marked increase of launching
# satellites for expanding internet access, but also for deep space
# telescopes, monitoring weather, and even ramping up moon exploration.

#   https://ourworldindata.org/grapher/yearly-number-of-objects-launched-into-outer-space
#   https://spacestatsonline.com/launches

# Caught up in the spirit, you look for opportunties to attend a rocket
# launch, and you decide to practice your countdown chant: 3...2...1...
# LIFTOFF!! In fact, why not enlist your computer in the spirit as well?

#   https://knowyourmeme.com/photos/872212

# The following function demonstrates a simple way to do so using the
# tools we already know. Given an initial number of seconds, we use a
# `range()` to decrement down to but not include zero. To wait the
# proper duration of one second between printing each number in the
# sequence, we include a `time.sleep()` in the loop body (which was
# demonstrated in Friday's recitation.). Finally, when the countdown
# reaches zero, we exit the loop and print a final valediction.


def countdown(n):
    for i in range(n, 0, -1):
        print(f"{i}...")
        time.sleep(1)
    print("LIFTOFF!!")


# countdown(5)


# This is a "global" view of the entire task, which is how we've written
# code so far. Our `countdown()` is relatively simple, but basically we
# first take care of the countdown sequence, and at the end we announce
# the liftoff.

# However, we could also take a more "local" view of our task. Rather
# than worrying about a loop, we could start by just focusing on
# printing the first number in the countdown. Got it? Okay, phew! Time
# to sleep.

# One second later, we realize we haven't finished our task. For
# example, after counting 5..., we still need to count 4...3...etc. But
# that's just a countdown sequence starting from 4. So couldn't we
# achieve that just by calling `countdown(4)`, i.e., `countdown(n - 1)`?


def countdown(n):
    print(f"{n}...")
    time.sleep(1)
    countdown(n - 1)


# countdown(5)


# If you run the above function call, it will indeed count down properly
# from 5. Then it will continue counting 0...-1...-2...etc. This is
# because there is nothing to stop Python from creating more frames on
# the stack for each successive `countdown(n - 1)` call.

# If this code were running at NASA's launch control, green goo would
# leak out of their consoles as their computers ran out of memory, and
# the entire complex would be locked down and carted off to Area 51. (To
# prevent green goo from coming out of your own computer, press Ctrl+C,
# or in VS Code, click the kill/trash icon in the terminal pane.)

# Fortunately, there's a simple fix for all this. At the beginning of
# `countdown()`'s code, we check if `n` has reached zero. If so, then we
# forget about counting further and instead declare liftoff. Very
# importantly, to AVOID counting past zero, we return immediately upon
# liftoff, and we DO NOT run `countdown(-1)`.


def countdown(n):
    if n == 0:
        print("LIFTOFF!!")
        return
    print(f"{n}...")
    time.sleep(1)
    countdown(n - 1)


# countdown(5)


# EXERCISE:
# Having saved NASA from certain doom, it is worth reflecting on how
# Python runs this code. Starting with a smaller `n` (say, 3), draw out
# and trace through the environment diagram for a global-level call to
# `countdown(3)`. (Try it on paper first, and then check yourself with
# pythontutor.com.)

# You should see that there are four frames created, from `countdown(3)`
# to `countdown(0)`. When that last frame returns, control returns to
# the previous frame for `countdown(1)`. Then that frame exits and
# returns control to `countdown(2)`, etc.

# In effect, we're taking advantage of the function call stack to "roll
# out" a sequence, and our **base case** of `n == 0` signals when to
# stop rolling out new frames. Going down the stack, we only run code
# outside that `if` block, which is called the **recursive case,** until
# we reach the base case. Once that is properly handled, the explicit
# `return` causes the stack mechanism to start "undoing" the roll-out by
# removing frames back up the stack.

# Thus, we've used recursion to essentially accomplish iteration. If
# this is your first time analyzing recursion, it may seem less
# straightforward than just applying iteration. In this situation, it
# even cost us two more lines of code! However, the idea of solving a
# task (`countdown(n)`) by using a solution to a smaller task
# (`countdown(n - 1)`) can be a very useful -- and arguably elegant --
# way of breaking down complex problems.

# In practice, programmers may analyze certain problems recursively and
# then implement an iterative version for efficiency reasons. We'll see
# a few examples of this through the course.


############################################################
# computing factorials
############################################################


# EXERCISE:
# Okay, your turn! Consider the task of computing the factorial (of a
# positive integer). Here is an iterative implementation. Below that,
# complete the recursive implementation, and on your own, draw out the
# environment diagram when running each implementation. In this case,
# you should be able to complete the recursive implementation (base case
# and recursive case) in just three lines!


# iterative
def factorial(n):
    result = 1
    for k in range(2, n + 1):
        result *= k
    return result


# print(factorial(3))
# print(factorial(4))
# print(factorial(5))


# recursive
def factorial(n):
    ...  # TODO


# print(factorial(3))
# print(factorial(4))
# print(factorial(5))
