# ------------------------------------------------------------------------------
# Utility class to hold the local and built in methods separately.  Add all local
# methods FIRST, then add built ins.
# ------------------------------------------------------------------------------
class ScriptMethods:
	# List of methods that should not be overloaded when they are not defined
	# in the class being doubled.  These either break things if they are
	# overloaded or do not have a "super" equivalent so we can't just pass
	# through.
	var _blacklist = [
		'has_method',
		'get_script',
		'get',
		'_notification',
		'get_path',
		'_enter_tree',
		'_exit_tree',
		'_process',
		'_draw',
		'_physics_process',
		'_input',
		'_unhandled_input',
		'_unhandled_key_input',
		'_set',
		'_get', # probably
		'emit_signal', # can't handle extra parameters to be sent with signal.
		'draw_mesh', # issue with one parameter, value is `Null((..), (..), (..))``
		'_to_string', # nonexistant function ._to_string
	]

	var built_ins = []
	var local_methods = []
	var _method_names = []

	func is_blacklisted(method_meta):
		return _blacklist.find(method_meta.name) != -1

	func _add_name_if_does_not_have(method_name):
		var should_add = _method_names.find(method_name) == -1
		if(should_add):
			_method_names.append(method_name)
		return should_add

	func add_built_in_method(method_meta):
		var did_add = _add_name_if_does_not_have(method_meta.name)
		if(did_add and !is_blacklisted(method_meta)):
			built_ins.append(method_meta)

	func add_local_method(method_meta):
		var did_add = _add_name_if_does_not_have(method_meta.name)
		if(did_add):
			local_methods.append(method_meta)

	func to_s():
		var text = "Locals\n"
		for i in range(local_methods.size()):
			text += str("  ", local_methods[i].name, "\n")
		text += "Built-Ins\n"
		for i in range(built_ins.size()):
			text += str("  ", built_ins[i].name, "\n")
		return text

# ------------------------------------------------------------------------------
# Helper class to deal with objects and inner classes.
# ------------------------------------------------------------------------------
class ObjectInfo:
	var _path = null
	var _subpaths = []
	var _utils = load('res://addons/gut/utils.gd').new()
	var _method_strategy = null
	var make_partial_double = false
	var scene_path = null
	var _native_class = null
	var _native_class_instance = null

	func _init(path, subpath=null):
		_path = path
		if(subpath != null):
			_subpaths = _utils.split_string(subpath, '/')

	# Returns an instance of the class/inner class
	func instantiate():
		var to_return = null
		if(is_native()):
			to_return = _native_class.new()
		else:
			to_return = get_loaded_class().new()
		return to_return

	# Can't call it get_class because that is reserved so it gets this ugly name.
	# Loads up the class and then any inner classes to give back a reference to
	# the desired Inner class (if there is any)
	func get_loaded_class():
		var LoadedClass = load(_path)
		for i in range(_subpaths.size()):
			LoadedClass = LoadedClass.get(_subpaths[i])
		return LoadedClass

	func to_s():
		return str(_path, '[', get_subpath(), ']')

	func get_path():
		return _path

	func get_subpath():
		return _utils.join_array(_subpaths, '/')

	func has_subpath():
		return _subpaths.size() != 0

	func get_extends_text():
		var extend = null
		if(is_native()):
			extend = str("extends ", get_native_class_name())
		else:
			extend = str("extends '", get_path(), '\'')

		if(has_subpath()):
			extend += str('.', get_subpath().replace('/', '.'))

		return extend

	func get_method_strategy():
		return _method_strategy

	func set_method_strategy(method_strategy):
		_method_strategy = method_strategy

	func is_native():
		return _native_class != null

	func set_native_class(native_class):
		_native_class = native_class
		_native_class_instance = native_class.new()
		_path = _native_class_instance.get_class()

	func get_native_class_name():
		return _native_class_instance.get_class()

# ------------------------------------------------------------------------------
# START Doubler
# ------------------------------------------------------------------------------
var _utils = load('res://addons/gut/utils.gd').new()

var _output_dir = null
var _double_count = 0 # used in making files names unique
var _use_unique_names = true
var _spy = null
var  _ignored_methods = _utils.OneToMany.new()

var _stubber = _utils.Stubber.new()
var _lgr = _utils.get_logger()
var _method_maker = _utils.MethodMaker.new()
var _strategy = null


func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL):
	# make sure _method_maker gets logger too
	set_logger(_utils.get_logger())
	_strategy = strategy

# ###############
# Private
# ###############
func _get_indented_line(indents, text):
	var to_return = ''
	for i in range(indents):
		to_return += "\t"
	return str(to_return, text, "\n")


func _stub_to_call_super(obj_info, method_name):
	var path = obj_info.get_path()
	if(obj_info.scene_path != null):
		path = obj_info.scene_path
	var params = _utils.StubParams.new(path, method_name, obj_info.get_subpath())
	params.to_call_super()
	_stubber.add_stub(params)


func _write_file(obj_info, dest_path, override_path=null):
	var script_methods = _get_methods(obj_info)

	var metadata = _get_stubber_metadata_text(obj_info)
	if(override_path):
		metadata = _get_stubber_metadata_text(obj_info, override_path)

	var f = File.new()
	var f_result = f.open(dest_path, f.WRITE)

	if(f_result != OK):
		print('Error creating file ', dest_path)
		print('Could not create double for :', obj_info.to_s())
		return

	f.store_string(str(obj_info.get_extends_text(), "\n"))
	f.store_string(metadata)

	for i in range(script_methods.local_methods.size()):
		if(obj_info.make_partial_double):
			_stub_to_call_super(obj_info, script_methods.local_methods[i].name)
		f.store_string(_get_func_text(script_methods.local_methods[i]))

	for i in range(script_methods.built_ins.size()):
		_stub_to_call_super(obj_info, script_methods.built_ins[i].name)
		f.store_string(_get_func_text(script_methods.built_ins[i]))

	f.close()


func _double_scene_and_script(scene_info, dest_path):
	var dir = Directory.new()
	dir.copy(scene_info.get_path(), dest_path)

	var inst = load(scene_info.get_path()).instance()
	var script_path = null
	if(inst.get_script()):
		script_path = inst.get_script().get_path()
	inst.free()

	if(script_path):
		var oi = ObjectInfo.new(script_path)
		oi.set_method_strategy(scene_info.get_method_strategy())
		oi.make_partial_double = scene_info.make_partial_double
		oi.scene_path = scene_info.get_path()
		var double_path = _double(oi, scene_info.get_path())
		var dq = '"'

		var f = File.new()
		f.open(dest_path, f.READ)
		var source = f.get_as_text()
		f.close()

		source = source.replace(dq + script_path + dq, dq + double_path + dq)

		f.open(dest_path, f.WRITE)
		f.store_string(source)
		f.close()

	return script_path

func _get_methods(object_info):
	var obj = object_info.instantiate()
	# any method in the script or super script
	var script_methods = ScriptMethods.new()
	var methods = obj.get_method_list()

	# first pass is for local methods only
	for i in range(methods.size()):
		# 65 is a magic number for methods in script, though documentation
		# says 64.  This picks up local overloads of base class methods too.
		if(methods[i].flags == 65 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])):
			script_methods.add_local_method(methods[i])


	if(object_info.get_method_strategy() == _utils.DOUBLE_STRATEGY.FULL):
		# second pass is for anything not local
		for i in range(methods.size()):
			# 65 is a magic number for methods in script, though documentation
			# says 64.  This picks up local overloads of base class methods too.
			if(methods[i].flags != 65 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])):
				script_methods.add_built_in_method(methods[i])

	return script_methods

func _get_inst_id_ref_str(inst):
	var ref_str = 'null'
	if(inst):
		ref_str = str('instance_from_id(', inst.get_instance_id(),')')
	return ref_str

func _get_stubber_metadata_text(obj_info, override_path = null):
	var path = obj_info.get_path()
	if(override_path != null):
		path = override_path
	return "var __gut_metadata_ = {\n" + \
           "\tpath='" + path + "',\n" + \
		   "\tsubpath='" + obj_info.get_subpath() + "',\n" + \
		   "\tstubber=" + _get_inst_id_ref_str(_stubber) + ",\n" + \
		   "\tspy=" + _get_inst_id_ref_str(_spy) + "\n" + \
           "}\n"

func _get_spy_text(method_hash):
	var txt = ''
	if(_spy):
		var called_with = _method_maker.get_spy_call_parameters_text(method_hash)
		txt += "\t__gut_metadata_.spy.add_call(self, '" + method_hash.name + "', " + called_with + ")\n"
	return txt

func _get_func_text(method_hash):
	var ftxt = _method_maker.get_decleration_text(method_hash) + "\n"

	var called_with = _method_maker.get_spy_call_parameters_text(method_hash)
	ftxt += _get_spy_text(method_hash)

	if(_stubber and method_hash.name != '_init'):
		var call_method = _method_maker.get_super_call_text(method_hash)
		ftxt += "\tif(__gut_metadata_.stubber.should_call_super(self, '" + method_hash.name + "', " + called_with + ")):\n"
		ftxt += "\t\treturn " + call_method + "\n"
		ftxt += "\telse:\n"
		ftxt += "\t\treturn __gut_metadata_.stubber.get_return(self, '" + method_hash.name + "', " + called_with + ")\n"
	else:
		ftxt += "\tpass\n"

	return ftxt

func _get_super_func_text(method_hash):
	var call_method = _method_maker.get_super_call_text(method_hash)

	var call_super_text = str("return ", call_method, "\n")

	var ftxt = _method_maker.get_decleration_text(method_hash) + "\n"
	ftxt += _get_spy_text(method_hash)

	ftxt += _get_indented_line(1, call_super_text)

	return ftxt

# returns the path to write the double file to
func _get_temp_path(object_info):
	var file_name = null
	var extension = null
	if(object_info.is_native()):
		file_name = object_info.get_native_class_name()
		extension = 'gd'
	else:
		file_name = object_info.get_path().get_file().get_basename()
		extension = object_info.get_path().get_extension()

	if(object_info.has_subpath()):
		file_name += '__' + object_info.get_subpath().replace('/', '__')

	if(_use_unique_names):
		file_name += str('__dbl', _double_count, '__.', extension)
	else:
		file_name += '.' + extension

	var to_return = _output_dir.plus_file(file_name)
	return to_return

func _double(obj_info, override_path=null):
	var temp_path = _get_temp_path(obj_info)
	_write_file(obj_info, temp_path, override_path)
	_double_count += 1
	return temp_path


func _double_script(path, make_partial, strategy):
	var oi = ObjectInfo.new(path)
	oi.make_partial_double = make_partial
	oi.set_method_strategy(strategy)
	var to_return = load(_double(oi))

	return to_return

func _double_inner(path, subpath, make_partial, strategy):
	var oi = ObjectInfo.new(path, subpath)
	oi.set_method_strategy(strategy)
	oi.make_partial_double = make_partial
	var to_return = load(_double(oi))

	return to_return

func _double_scene(path, make_partial, strategy):
	var oi = ObjectInfo.new(path)
	oi.set_method_strategy(strategy)
	oi.make_partial_double = make_partial
	var temp_path = _get_temp_path(oi)
	_double_scene_and_script(oi, temp_path)

	return load(temp_path)

func _double_gdnative(native_class, make_partial, strategy):
	var oi = ObjectInfo.new(null)
	oi.set_native_class(native_class)
	oi.set_method_strategy(strategy)
	oi.make_partial_double = make_partial
	var to_return = load(_double(oi))

	return to_return



# ###############
# Public
# ###############
func get_output_dir():
	return _output_dir

func set_output_dir(output_dir):
	_output_dir = output_dir
	var d = Directory.new()
	d.make_dir_recursive(output_dir)

func get_spy():
	return _spy

func set_spy(spy):
	_spy = spy

func get_stubber():
	return _stubber

func set_stubber(stubber):
	_stubber = stubber

func get_logger():
	return _lgr

func set_logger(logger):
	_lgr = logger
	_method_maker.set_logger(logger)

func get_strategy():
	return _strategy

func set_strategy(strategy):
	_strategy = strategy

func partial_double_scene(path, strategy=_strategy):
	return _double_scene(path, true, strategy)

# double a scene
func double_scene(path, strategy=_strategy):
	return _double_scene(path, false, strategy)

# double a script/object
func double(path, strategy=_strategy):
	return _double_script(path, false, strategy)

func partial_double(path, strategy=_strategy):
	return _double_script(path, true, strategy)

func partial_double_inner(path, subpath, strategy=_strategy):
	return _double_inner(path, subpath, true, strategy)

# double an inner class in a script
func double_inner(path, subpath, strategy=_strategy):
	return _double_inner(path, subpath, false, strategy)

# must always use FULL strategy since this is a native class and you won't get
# any methods if you don't use FULL
func double_gdnative(native_class):
	return _double_gdnative(native_class, false, _utils.DOUBLE_STRATEGY.FULL)

# must always use FULL strategy since this is a native class and you won't get
# any methods if you don't use FULL
func partial_double_gdnative(native_class):
	return _double_gdnative(native_class, true, _utils.DOUBLE_STRATEGY.FULL)

func clear_output_directory():
	var did = false
	if(_output_dir.find('user://') == 0):
		var d = Directory.new()
		var result = d.open(_output_dir)
		# BIG GOTCHA HERE.  If it cannot open the dir w/ erro 31, then the
		# directory becomes res:// and things go on normally and gut clears out
		# out res:// which is SUPER BAD.
		if(result == OK):
			d.list_dir_begin(true)
			var f = d.get_next()
			while(f != ''):
				d.remove(f)
				f = d.get_next()
				did = true
	return did

func delete_output_directory():
	var did = clear_output_directory()
	if(did):
		var d = Directory.new()
		d.remove(_output_dir)

# When creating doubles a unique name is used that each double can be its own
# thing.  Sometimes, for testing, we do not want to do this so this allows
# you to turn off creating unique names for each double class.
#
# THIS SHOULD NEVER BE USED OUTSIDE OF INTERNAL GUT TESTING.  It can cause
# weird, hard to track down problems.
func set_use_unique_names(should):
	_use_unique_names = should

func add_ignored_method(path, method_name):
	_ignored_methods.add(path, method_name)

func get_ignored_methods():
	return _ignored_methods
