import csv
import sys

from util import Node, StackFrontier, QueueFrontier

# Maps names to a set of corresponding person_ids
names = {}

# Maps person_ids to a dictionary of: name, birth, movies (a set of movie_ids)
people = {}

# Maps movie_ids to a dictionary of: title, year, stars (a set of person_ids)
movies = {}


def load_data(directory):
	"""
	Load data from CSV files into memory.
	"""
	# Load people
	with open(f"{directory}/people.csv", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		for row in reader:
			people[row["id"]] = {
				"name": row["name"],
				"birth": row["birth"],
				"movies": set()
			}
			if row["name"].lower() not in names:
				names[row["name"].lower()] = {row["id"]}
			else:
				names[row["name"].lower()].add(row["id"])

	# Load movies
	with open(f"{directory}/movies.csv", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		for row in reader:
			movies[row["id"]] = {
				"title": row["title"],
				"year": row["year"],
				"stars": set()
			}

	# Load stars
	with open(f"{directory}/stars.csv", encoding="utf-8") as f:
		reader = csv.DictReader(f)
		for row in reader:
			try:
				people[row["person_id"]]["movies"].add(row["movie_id"])
				movies[row["movie_id"]]["stars"].add(row["person_id"])
			except KeyError:
				pass


def main():
	if len(sys.argv) > 2:
		sys.exit("Usage: python degrees.py [directory]")
	directory = sys.argv[1] if len(sys.argv) == 2 else "large"

	# Load data from files into memory
	print("Loading data...")
	load_data(directory)
	print("Data loaded.")

	source = person_id_for_name(input("Name: "))
	if source is None:
		sys.exit("Person not found.")
	target = person_id_for_name(input("Name: "))
	if target is None:
		sys.exit("Person not found.")

	path = shortest_path(source, target)

	if path is None:
		print("Not connected.")
	else:
		degrees = len(path)
		print(f"{degrees} degrees of separation.")
		path = [(None, source)] + path
		for i in range(degrees):
			person1 = people[path[i][1]]["name"]
			person2 = people[path[i + 1][1]]["name"]
			movie = movies[path[i + 1][0]]["title"]
			print(f"{i + 1}: {person1} and {person2} starred in {movie}")


def shortest_path(source, target):
	"""
	Returns the shortest list of (movie_id, person_id) pairs
	that connect the source to the target.

	If no possible path, returns None.
	"""
	frontier = QueueFrontier()
	explored = StackFrontier()
	# Create our initial node, since this is the starting node there will be no parent node.
	# No need to worry about populating a movie_id.
	frontier.add(Node(source, None, None))
	results = []
	
	while not frontier.empty():
		currentNode = frontier.remove()
		# Is the currentNode's state (person_id) equal to the target (also a person_id)?
		if currentNode.state == target:
			tmp = currentNode
			# If so, loop over the currentNode's parents, grandparents and so forth until we hit a node
			# that has no parent node.
			while tmp:
				# If the parent node is None, then that means we've hit the last node in the list.
				# So, we won't be adding this tmp node to the result listing as it is the source.
				if not tmp.parent == None:
					results.append((tmp.action, tmp.state))
					
				tmp = tmp.parent
			# Since we traversed the target node's parents from bottom up, so reverse the list to present the order of the connections
			# in the order the caller code is expecting.
			results.reverse()
			print(results)
			break
		
		# currentNode.state has person ID.
		neighbors = neighbors_for_person(currentNode.state)
		# currentNode has been explored, so add it it the explored stack.
		explored.add(currentNode)
		
		for i in neighbors:
			# i is a pair (movie_id, person_id), so i[i] contains the person_id.
			if not explored.contains_state(i[1]):
				# i[0] contains movie_id (this seemed like the best thing to use for the "action" parameter of the Node object for)
				# currentNode is being set as the parent node of the new node being added to the frontier
				# i[1] contains person_id 
				frontier.add(Node(i[1], currentNode, i[0]))
			
	return None

def person_id_for_name(name):
	"""
	Returns the IMDB id for a person's name,
	resolving ambiguities as needed.
	"""
	person_ids = list(names.get(name.lower(), set()))
	if len(person_ids) == 0:
		return None
	elif len(person_ids) > 1:
		print(f"Which '{name}'?")
		for person_id in person_ids:
			person = people[person_id]
			name = person["name"]
			birth = person["birth"]
			print(f"ID: {person_id}, Name: {name}, Birth: {birth}")
		try:
			person_id = input("Intended Person ID: ")
			if person_id in person_ids:
				return person_id
		except ValueError:
			pass
		return None
	else:
		return person_ids[0]


def neighbors_for_person(person_id):
	"""
	Returns (movie_id, person_id) pairs for people
	who starred with a given person.
	"""
	movie_ids = people[person_id]["movies"]
	neighbors = set()
	for movie_id in movie_ids:
		for person_id in movies[movie_id]["stars"]:
			neighbors.add((movie_id, person_id))
	return neighbors


if __name__ == "__main__":
	main()
