extends Node
class_name YarnImporter

#
# A YARN Importer for Godot
#
# Credits: 
# - Dave Kerr (http://www.naturallyintelligent.com)
# 
# Latest: https://github.com/naturally-intelligent/godot-yarn-importer
# 
# Yarn: https://github.com/InfiniteAmmoInc/Yarn
# Twine: http://twinery.org
# 
# Yarn: a ball of threads (Yarn file)
# Thread: a series of fibres (Yarn node)
# Fibre: a text or choice or logic (Yarn line)

var yarn = {}

# OVERRIDE METHODS
#
# called to request new dialog
func on_new_line(text):
	pass
	
# called to request new choice button
func on_choices(choices_list):
	pass

# called to request internal logic handling
func logic(instruction, command):
	pass
	
# called for each line of text
func yarn_text_variables(text):
	return text
	
# called when "settings" node parsed
func story_setting(setting, value):
	pass
	
# called for each node name
func on_node_start(to):
	pass
	yield(get_tree(), "idle_frame")

# called for each node name (after)
func on_node_end(to):
	pass
	yield(get_tree(), "idle_frame")

# START SPINNING YOUR YARN
#
func spin_yarn(file, start_thread = false):
	yarn = load_yarn(file)
	# Find the starting thread...
	if not start_thread:
		start_thread = yarn['start']
	# Load any scene-specific settings
	# (Not part of official Yarn standard)
	if 'settings' in yarn['threads']:
		var settings = yarn['threads']['settings']
		for fibre in settings['fibres']:
			var line = fibre['text']
			var split = line.split('=')
			var setting = split[0].strip_edges(true, true)
			var value = split[1].strip_edges(true, true)
			story_setting(setting, value)
	# First thread unravel...
	yield(on_dialogue_start(), "completed")
	yield(yarn_unravel(start_thread), "completed")

# Internally create a new thread (during loading)
func new_yarn_thread():
	var thread = {}
	thread['title'] = ''
	thread['kind'] = 'branch' # 'branch' for standard dialog, 'code' for gdscript
	thread['tags'] = [] # unused
	thread['fibres'] = []
	return thread

func exec(stri):
	var expr := Expression.new()
	print("input : " + stri)
	var err := expr.parse(stri)
	if err == OK:
		var res = expr.execute()
		print("res : " + str(res))
	else:
		print("err : " + str(err))

func jajoujaj():
	print("jaaaja")
	
# Internally create a new fibre (during loading)
func new_yarn_fibre(line:String):
	# choice fibre
	if line.substr(0,2) == '[[':
		if line.find('|') != -1:
			var fibre = {}
			fibre['kind'] = 'choice'
			line = line.replace('[[', '')
			line = line.replace(']]', '')
			var split = line.split('|')
			fibre['text'] = split[0]
			fibre['marker'] = split[1]
			return fibre
		else:
			var fibre = {}
			fibre['kind'] = 'jump'
			line = line.replace('[[', '')
			line = line.replace(']]', '')
			fibre['marker'] = line
			return fibre
	# logic instruction (not part of official Yarn standard)
	elif line.substr(0,2) == '<<':
		line = line.replace('<<', '')
		line = line.replace('>>', '')
		var line_split = line.split(' ')
		print (line_split)
		var command = line.substr(line.find(' '))
		exec(command)
		
#		if line.find(':') != -1:
#			fibre['kind'] = 'logic'
#			line = line.replace('<<', '')
#			line = line.replace('>>', '')
#			var split = line.split(':')
#			fibre['instruction'] = split[0]
#			fibre['command'] = split[1]
#			#print(line, split[0], split[1])
#			return fibre
	# text fibre
	var fibre = {}
	fibre['kind'] = 'text'
	fibre['text'] = line
	return fibre

# Create Yarn data structure from file (must be *.yarn.txt Yarn format)
func load_yarn(path):
	var yarn = {}
	yarn['threads'] = {}
	yarn['start'] = false
	yarn['file'] = path
	var file = File.new()
	file.open(path, file.READ)
	if file.is_open():
		# yarn reading flags
		var start = false
		var header = true
		var thread = new_yarn_thread()
		# loop
		while !file.eof_reached():
			# read a line
			var line = file.get_line()
			# header read mode
			if header:
				if line == '---':
					header = false
				else:
					var split = line.split(': ')
					if split[0] == 'title':
						var title_split = split[1].split(':')
						var thread_title = ''
						var thread_kind = 'branch'
						if len(title_split) == 1:
							thread_title = split[1]
						else:
							thread_title = title_split[1]
							thread_kind = title_split[0]
						thread['title'] = thread_title
						thread['kind'] = thread_kind
						if not yarn['start']:
							yarn['start'] = thread_title
			# end of thread
			elif line == '===':
				header = true
				yarn['threads'][thread['title']] = thread
				thread = new_yarn_thread()
			# fibre read mode
			else:
				var fibre = new_yarn_fibre(line)
				if fibre:
					thread['fibres'].append(fibre)
	else:
		print('ERROR: Yarn file missing: ', filename)
	return yarn

# Main logic for node handling
#
func yarn_unravel(to, from=false):	
	if not to in yarn['threads']:
		print('WARNING: Missing Yarn thread: ', to, ' in file ',yarn['file'])
		return
		
	while to != null and to in yarn['threads']:
		yield (on_node_start(to), "completed")
		if to in yarn['threads']:
			var thread = yarn['threads'][to]
			to = null
			match thread['kind']:
				'branch':
					var i = 0
					while i < thread['fibres'].size():
						match thread['fibres'][i]['kind']:
							'text':
								var fibre = thread['fibres'][i]
								var text = yarn_text_variables(fibre['text'])
								yield(on_new_line(text), "completed")
							'choice':
								var choices = []
								while i < thread['fibres'].size() and thread['fibres'][i]['kind'] == 'choice' :
									var fibre = thread['fibres'][i]
									var text = yarn_text_variables(fibre['text'])
									choices.push_back({"text": text, "marker": fibre['marker']})
									i += 1
								to = yield(on_choices(choices), "completed")
								break
							'logic':
								var fibre = thread['fibres'][i]
								var instruction = fibre['instruction']
								var command = fibre['command']
								yield(logic(instruction, command), "completed")
							'jump':
								var fibre = thread['fibres'][i]
								to = fibre['marker']
								break
						i += 1
				'code':
					yarn_code(to)
		yield(on_node_end(to), "completed")
		
	yield (on_dialogue_end(), "completed")

func on_dialogue_start():
	pass
	yield(get_tree(), "idle_frame")

func on_dialogue_end():
	pass
	yield(get_tree(), "idle_frame")

#
# RUN GDSCRIPT CODE FROM YARN NODE - Special node = code:title
# - Not part of official Yarn standard
#
func yarn_code(title, run=true, parent='parent.', tabs="\t", next_func="yarn_unravel"):
	if title in yarn['threads']:
		var thread = yarn['threads'][title]
		var code = ''
		for fibre in thread['fibres']:
			match fibre['kind']:
				'text':
					var line = yarn_text_variables(fibre['text'])
					line = yarn_code_replace(line, parent, next_func)
					code += tabs + line + "\n"
				'choice':
					var line = parent+next_func+"('"+fibre['marker']+"')"
					print(line)
					code += tabs + line + "\n"
		if run:
			run_yarn_code(code)
		else:
			return code
	else:
		print('WARNING: Title missing in yarn ball: ', title)

# override to replace convenience variables
func yarn_code_replace(code, parent='parent.', next_func="yarn_unravel"):
	if code.find("[[") != -1:
		code = code.replace("[[", parent+next_func+"('")
		code = code.replace("]]", "')")
	code = code.replace("say(", parent+"say(")
	code = code.replace("choice(", parent+"choice(")
	return code

func run_yarn_code(code):
	var front = "extends Node\n"
	front += "func dynamic_code():\n"
	front += "\tvar parent = get_parent()\n\n"
	code = front + code
	#print("CODE BLOCK: \n", code)

	var script = GDScript.new()
	script.set_source_code(code)
	script.reload()

	#print("Executing code...")
	var node = Node.new()
	node.set_script(script)
	add_child(node)
	var result = node.dynamic_code()
	remove_child(node)

	return result

# EXPORTING TO GDSCRIPT
#
# This code may not be directly usable
# Use if you need an exit from Yarn

func export_to_gdscript():
	var script = ''
	script += "func start_story():\n\n"
	if 'settings' in yarn['threads']:
		var settings = yarn['threads']['settings']
		for fibre in settings['fibres']:
			var line = fibre['text']
			var split = line.split('=')
			var setting = split[0].strip_edges(true, true)
			var value = split[1].strip_edges(true, true)
			script += "\t" + 'story_setting("' + setting + '", "' + value + '")' + "\n"
	script += "\tstory_logic('" + yarn['start'] + "')\n\n"
	# story logic choice/press event
	script += "func story_logic(marker):\n\n"
	script += "\tmatch marker:\n"
	for title in yarn['threads']:
		var thread = yarn['threads'][title]
		match thread['kind']:
			'branch':
				var code = "\n\t\t'" + thread['title'] + "':"
				var tabs = "\n\t\t\t"
				for fibre in thread['fibres']:
					match fibre['kind']:
						'text':
							code += tabs + 'say("' + fibre['text'] + '")'
						'choice':
							code += tabs + 'choice("' + fibre['text'] + '", "' + fibre['marker'] + '")'
						'logic':
							code += tabs + 'logic("' + fibre['instruction'] + '", "' + fibre['command'] + '")'
				script += code + "\n"
			'code':
				var code = "\n\t\t'" + thread['title'] + "':"
				var tabs = "\n\t\t\t"
				code += "\n"
				code += yarn_code(thread['title'], false, '', "\t\t\t", "story_logic")
				script += code + "\n"
	# done
	return script

func print_gdscript_to_console():
	print(export_to_gdscript())

func save_to_gdscript(filename):
	var script = export_to_gdscript()
	# write to file
	var file = File.new()
	file.open(filename, file.WRITE)
	if not file.is_open():
		print('ERROR: Cant open file ', filename)
		return false
	file.store_string(script)
	file.close()

