#!/usr/bin/env python3

# PySimpleGUI call reference manual: https://www.pysimplegui.org/en/latest/call%20reference/

import PySimpleGUI as sg
import json
from os import listdir
from os.path import isfile, join, isdir, exists

# ------------------------------------------------------------------------------------------- Config
font_size = 9


# ---------------------------------------------------------------------------------------- Utilities
def load_json(path):
	if exists(path):
		with open(path) as f:
			return json.load(f)
	return {}


def compare_arrays(arr_a, arr_b):
	if len(arr_a) != len(arr_b):
		return False
	
	for i in range(len(arr_a)):
		if arr_a[i] != arr_b[i]:
			return False
	
	return True


def repeat_characters(char, n):
	s = ""
	for i in range(0, n):
		s += char
	return s


# Fetches the directories containing the frame info.
directories = [f for f in listdir("./") if (isdir(join("./", f)) and len(listdir(join("./", f))) > 0)]

# ----------------------------------------------------------------------------------------------- UI
def create_layout():

	# Fetch the frame count from the dirs.
	frame_count = -1 
	frames_iterations = {}
	frames_description = {}
	for dir in directories:
		dir_path = join("./", dir)
		file_names = [f for f in listdir(dir_path) if isfile(join(dir_path, f))]
		for file_name in file_names:
			file_extension_index = file_name.find("fd-")
			if file_extension_index == 0:
				# This file contains information about the frame.
				# Extract the frame index
				frame_iteration = file_name.count("@") + 1
				if frame_iteration > 1:
					frame_index = int(file_name[3:file_name.index("@")])
				else:
					frame_index = int(file_name[3:file_name.index(".json")])

				if frame_index in frames_iterations:
					frames_iterations[frame_index] = max(frame_iteration, frames_iterations[frame_index])
				else:
					frames_iterations[frame_index] = frame_iteration

				frame_count = max(frame_count, frame_index)

				file_path = join(dir_path, file_name)
				file_json = load_json(file_path)
				if "frame_summary" in file_json:
					if frame_index in frames_description:
						frames_description[frame_index] += " " + file_json["frame_summary"]
					else:
						frames_description[frame_index] = file_json["frame_summary"]
				else:
					frames_description[frame_index] = ""

	frame_count += 1


	# --- UI - Compose timeline ---
	frame_list_values = []
	for frame_index in range(frame_count):
		frame_description = frames_description[frame_index]
		frame_iterations = frames_iterations[frame_index]
		for i in range(0, frame_iterations):
			if i == 0:
				frame_list_values.append("# " + str(frame_index) + " - " + frame_description)
			else:
				frame_list_values.append("# " + str(frame_index) + " @" + str(i + 1) + " - " + frame_description)

	# Release this array, we don't need anylonger.
	frames_description.clear()

	frames_list = sg.Listbox(frame_list_values, key="FRAMES_LIST", size = [45, 30], enable_events=True, horizontal_scroll=True, select_mode=sg.LISTBOX_SELECT_MODE_BROWSE)
	frames_list = sg.Frame("Frames", layout=[[frames_list]], vertical_alignment="top")

	# --- UI - Compose frame detail ---
	# Node list
	nodes_list_listbox = sg.Listbox([], key="NODE_LIST",  size = [45, 0], enable_events=True, horizontal_scroll=True, expand_y=True, expand_x=True, select_mode=sg.LISTBOX_SELECT_MODE_MULTIPLE)
	nodes_list_listbox = sg.Frame("Nodes", layout=[[nodes_list_listbox]], vertical_alignment="top", expand_y=True, expand_x=True);

	# Selected nodes title.
	node_tile_txt = sg.Text("", key="FRAME_SUMMARY", font="Any, " + str(font_size - 1), justification="left", border_width=1, text_color="dark red")

	# States table
	table_status_header = ["name"]
	table_status_widths = [30]
	for dir in directories:
		table_status_header.append("Begin ["+dir+"]")
		table_status_widths.append(30)

	for dir in directories:
		table_status_header.append("End ("+dir+")")
		table_status_widths.append(30)

	table_status = sg.Table([], table_status_header, key="TABLE_STATUS", justification='left', auto_size_columns=False, col_widths=table_status_widths, vertical_scroll_only=False, num_rows=38)
	table_status = sg.Frame("States", layout=[[table_status]], vertical_alignment="top")

	# Messages table
	tables_logs = []
	for dir in directories:
		tables_logs.append(sg.Frame("Log: " + dir + " Iteration: ", key=dir+"_FRAME_TABLE_LOG", layout=[[sg.Table([], [" #", "Log"], key=dir+"_TABLE_LOG", justification='left', auto_size_columns=False, col_widths=[4, 70], vertical_scroll_only=False, num_rows=25)]], vertical_alignment="top"))

	logs = sg.Frame("Messages", layout=[tables_logs], vertical_alignment="top")

	# --- UI - Main Window ---
	layout = [
	  [
			sg.Frame("", [[frames_list], [nodes_list_listbox]], vertical_alignment="top", expand_y=True),
			sg.Frame("Frame detail", [[node_tile_txt], [table_status], [logs]], key="FRAME_FRAME_DETAIL", vertical_alignment="top")
		],
		[
			sg.Button("Exit")
		]
	]

	return layout


# ------------------------------------------------------------------------------------ Create window
window = sg.Window(title="Network Synchronizer Debugger.", layout=create_layout(), margins=(5, 5), font="Any, " + str(font_size), resizable=True)


# ----------------------------------------------------------------------------------- Event handling
frame_data = {}
nodes_list = []
selected_nodes = []
used_frame_iteration = {}

while True:
	event, event_values = window.read()

	# EVENT: Close the program.
	if event == "Exit" or event == sg.WIN_CLOSED:
		window.close()
		break

	# EVENT: Show frame
	if event == "FRAMES_LIST":
		window["NODE_LIST"].update([])

		if event_values["FRAMES_LIST"] != []:
			frame_description = event_values["FRAMES_LIST"][0]
			if "@" in frame_description:
				frame_iteration = int(frame_description[frame_description.index(" @") + 2:frame_description.index(" - ")])
				selected_frame_index = int(frame_description[2:frame_description.index(" @")])
			else:
				frame_iteration = 1
				selected_frame_index = int(frame_description[2:frame_description.index(" - ")])

			print("Show frame: ", selected_frame_index, " Iteration: ", frame_iteration)

			frame_data = {}
			nodes_list = []
			used_frame_iteration = {}
			for dir in directories:
				used_frame_iteration[dir] = frame_iteration
				frame_file_path = join("./", dir, "fd-" + str(selected_frame_index) + repeat_characters("@", frame_iteration - 1) + ".json")

				if not exists(frame_file_path):
					print("The path: ", frame_file_path, " was not found. Falling back to no iteration path.")
					used_frame_iteration[dir] = 1
					frame_file_path = join("./", dir, "fd-" + str(selected_frame_index) + ".json")
				print("Path: ", frame_file_path)

				if exists(frame_file_path):
					frame_data_json = load_json(frame_file_path)
					frame_data[dir] = frame_data_json

					for node_path in frame_data_json["begin_state"]:
						if node_path not in nodes_list:
							# Add this node to the nodelist
							nodes_list.append(node_path)

					for node_path in frame_data_json["end_state"]:
						if node_path not in nodes_list:
							# Add this node to the nodelist
							nodes_list.append(node_path)

					for node_path in frame_data_json["node_log"]:
						if node_path not in nodes_list:
							# Add this node to the nodelist
							nodes_list.append(node_path)


			# Update the node list.
			window["NODE_LIST"].update(nodes_list)

		if len(selected_nodes) == 0:
			if len(nodes_list) > 0:
				selected_nodes = [nodes_list[0]]
			else:
				selected_nodes = []

		window["NODE_LIST"].set_value(selected_nodes)
		event = "NODE_LIST"
		event_values = {"NODE_LIST": selected_nodes}

	# EVENT: Show node data
	if event == "NODE_LIST":

		window["FRAME_SUMMARY"].update("")
		window["TABLE_STATUS"].update([])

		for dir_name in directories:
			window[dir_name + "_FRAME_TABLE_LOG"].update("Log: " + dir_name + "Frame: " + str(selected_frame_index) + " Iteration: " + str(used_frame_iteration[dir_name]))
			window[dir_name + "_TABLE_LOG"].update([["", "[Nothing for this node]"]])

		if event_values["NODE_LIST"] != []:
			instances_count = len(directories)
			row_size = 1 + (instances_count * 2)

			# Compose the status table
			states_table_values = []
			states_row_colors = []
			table_logs = {}
			log_row_colors = {}

			selected_nodes = event_values["NODE_LIST"]

			for node_path in selected_nodes:

				# First collects the var names
				vars_names = ["***"]
				for dir in directories:
					if dir in frame_data:
						if "begin_state" in frame_data[dir]:
							if node_path in frame_data[dir]["begin_state"]:
								for var_name in frame_data[dir]["begin_state"][node_path]:
									if var_name not in vars_names:
										vars_names.append(var_name)

						if "end_state" in frame_data[dir]:
							if node_path in frame_data[dir]["end_state"]:
								for var_name in frame_data[dir]["end_state"][node_path]:
									if var_name not in vars_names:
										vars_names.append(var_name)

				vars_names.append("---")

				# Add those to the table.
				for var_name in vars_names:

					# Initializes the row.
					row = [""] * row_size
					row_index = len(states_table_values)

					# Special rows
					if var_name == "***":
						# This is a special row to signal the start of a new node data
						row[0] = node_path
						states_table_values.append(row)
						states_row_colors.append((row_index, "black"))
						continue
					elif var_name == "---":
						# This is a special row to signal the end of the node data
						states_table_values.append(row)
						continue

					row[0] = var_name.replace("*", "🔄")

					# Set the row data.
					for dir_i, dir_name in enumerate(directories):
						if dir_name in frame_data:
							if "begin_state" in frame_data[dir_name]:
								if node_path in frame_data[dir_name]["begin_state"]:
									if var_name in frame_data[dir_name]["begin_state"][node_path]:
										#print(1, " + (", instances_count, " * 0) + ", dir_i)
										row[1 + (instances_count * 0) + dir_i] = str(frame_data[dir_name]["begin_state"][node_path][var_name])

							if "end_state" in frame_data[dir_name]:
								if node_path in frame_data[dir_name]["end_state"]:
									if var_name in frame_data[dir_name]["end_state"][node_path]:
										#print(1, " + (", instances_count, " * 1) + ", dir_i)
										row[1 + (instances_count * 1) + dir_i] = str(frame_data[dir_name]["end_state"][node_path][var_name])

					# Check if different, so mark a worning.
					for state_index in range(2):
						for i in range(instances_count - 1):
							if row[1 + (state_index*instances_count) + i + 0] != row[1 + (state_index*instances_count) + i + 1]:
								row[1 + (state_index*instances_count) + i + 0] = "⚠️ " + row[1 + (state_index*instances_count) + i + 0]
								row[1 + (state_index*instances_count) + i + 1] = "⚠️ " + row[1 + (state_index*instances_count) + i + 1]
								states_row_colors.append((row_index, "dark salmon"))
								break

					states_table_values.append(row)

				# Compose the log
				for dir_name in directories:
					if dir_name in frame_data:
						if "node_log" in frame_data[dir_name]:
							if node_path in frame_data[dir_name]["node_log"]:

								table_logs[dir_name] = table_logs.get(dir_name, [])
								log_row_colors[dir_name] = log_row_colors.get(dir_name, [])

								table_logs[dir_name] += [["", node_path]]
								log_row_colors[dir_name] += [(len(table_logs[dir_name]) - 1, "black")]

								for val in frame_data[dir_name]["node_log"][node_path]:

									# Append the log
									table_logs[dir_name] += [["{:4d}".format(val["i"]), val["m"]]]
									row_index = len(table_logs[dir_name]) - 1

									# Check if this line should be colored
									if val["m"].find("[WARNING]") == 0:
										log_row_colors[dir_name] += [(row_index, "dark salmon")]

									elif val["m"].find("[ERROR]") == 0:
										log_row_colors[dir_name] += [(row_index, "red")]

									elif val["m"].find("[WRITE]") == 0:
										log_row_colors[dir_name] += [(row_index, "cadet blue")]

									elif val["m"].find("[READ]") == 0:
										log_row_colors[dir_name] += [(row_index, "medium aquamarine")]

								table_logs[dir_name] += [["", ""]]

			window["FRAME_FRAME_DETAIL"].update("Frame " + str(selected_frame_index) + " details")
			window["TABLE_STATUS"].update(states_table_values, row_colors=states_row_colors)

			frame_summary = ""
			for dir_name in directories:
				if dir_name in frame_data:
					frame_summary += frame_data[dir_name]["frame_summary"]
				if dir_name in table_logs:
					window[dir_name + "_TABLE_LOG"].update(table_logs[dir_name], row_colors=log_row_colors[dir_name]);
			
			# Check if write and read databuffer is the same.
			for dir_name in directories:
				if dir_name in frame_data and (dir_name == "nonet" or dir_name == "client"):
					are_the_same = compare_arrays(frame_data[dir_name]["data_buffer_writes"], frame_data[dir_name]["data_buffer_reads"])
					if not are_the_same:
						if frame_summary != "":
							frame_summary += "\n"
						frame_summary += "[BUG] The DataBuffer written by `_collect_inputs` and read by `_controller_process` is different. Both should be exactly the same."

			# Check if the server read correctly the received buffer.
			if "server" in frame_data and "client" in frame_data:
				are_the_same = compare_arrays(frame_data["server"]["data_buffer_reads"], frame_data["client"]["data_buffer_reads"])
				if not are_the_same:
					if frame_summary != "":
						frame_summary += "\n"
					frame_summary += "[BUG] The DataBuffer written by the client is different from the one read on the server."

			# Check if the client sent the correct inputs to the server.
			if "client" in frame_data:
				for other_frame_index, is_similar in frame_data["client"]["are_inputs_different_results"].items():
					other_frame_index = int(other_frame_index)
					other_file_path = join("./", "client", "fd-" + str(other_frame_index) + ".json")
					other_frame_json = load_json(other_file_path)
					if "data_buffer_reads" in other_frame_json:
						is_really_similar = compare_arrays(other_frame_json["data_buffer_reads"], frame_data["client"]["data_buffer_reads"])
						if is_really_similar != is_similar:
							if frame_summary != "":
								frame_summary += "\n"
							frame_summary += "[BUG] The function `_are_inputs_different` doesn't seems to work:\n"
							frame_summary += "      As the inputs read on the frame " + str(frame_data["client"]["frame"]) + " and " + str(other_frame_index) + " are " + ("SIMILAR" if is_really_similar else "DIFFERENT") +" but the net sync considered it "+ ("SIMILAR" if is_similar else "DIFFERENT")

			window["FRAME_SUMMARY"].update(frame_summary)


# --------------------------------------------------------------------------------------------- Exit