@tool
extends Object
class_name Log

## helpers ####################################

static func assoc(opts: Dictionary, key: String, val):
	var _opts = opts.duplicate(true)
	_opts[key] = val
	return _opts

## config ####################################

static var config = {
	max_array_size=20,
	dictionary_skip_keys=["layer_0/tile_data"],
	}

static func get_max_array_size():
	return Log.config.get("max_array_size", 20)

static func get_dictionary_skip_keys():
	return Log.config.get("dictionary_skip_keys", [])

static func set_color_scheme(scheme):
	Log.config["color_scheme"] = scheme

static func get_config_color_scheme():
	return Log.config.get("color_scheme", {})

## colors ###########################################################################

# terminal safe colors:
# - black
# - red
# - green
# - yellow
# - blue
# - magenta
# - pink
# - purple
# - cyan
# - white
# - orange
# - gray

static var COLORS_TERMINAL_SAFE = {
	"SRC": "cyan",
	"ADDONS": "red",
	"TEST": "green",
	",": "red",
	"(": "red",
	")": "red",
	"[": "red",
	"]": "red",
	"{": "red",
	"}": "red",
	"&": "orange",
	"^": "orange",
	"dict_key": "magenta",
	"vector_value": "green",
	"class_name": "magenta",
	TYPE_NIL: "pink",
	TYPE_BOOL: "pink",
	TYPE_INT: "green",
	TYPE_FLOAT: "green",
	TYPE_STRING: "pink",
	TYPE_VECTOR2: "green",
	TYPE_VECTOR2I: "green",
	TYPE_RECT2: "green",
	TYPE_RECT2I: "green",
	TYPE_VECTOR3: "green",
	TYPE_VECTOR3I: "green",
	TYPE_TRANSFORM2D: "pink",
	TYPE_VECTOR4: "green",
	TYPE_VECTOR4I: "green",
	TYPE_PLANE: "pink",
	TYPE_QUATERNION: "pink",
	TYPE_AABB: "pink",
	TYPE_BASIS: "pink",
	TYPE_TRANSFORM3D: "pink",
	TYPE_PROJECTION: "pink",
	TYPE_COLOR: "pink",
	TYPE_STRING_NAME: "pink",
	TYPE_NODE_PATH: "pink",
	TYPE_RID: "pink",
	TYPE_OBJECT: "pink",
	TYPE_CALLABLE: "pink",
	TYPE_SIGNAL: "pink",
	TYPE_DICTIONARY: "pink",
	TYPE_ARRAY: "pink",
	TYPE_PACKED_BYTE_ARRAY: "pink",
	TYPE_PACKED_INT32_ARRAY: "pink",
	TYPE_PACKED_INT64_ARRAY: "pink",
	TYPE_PACKED_FLOAT32_ARRAY: "pink",
	TYPE_PACKED_FLOAT64_ARRAY: "pink",
	TYPE_PACKED_STRING_ARRAY: "pink",
	TYPE_PACKED_VECTOR2_ARRAY: "pink",
	TYPE_PACKED_VECTOR3_ARRAY: "pink",
	TYPE_PACKED_COLOR_ARRAY: "pink",
	TYPE_MAX: "pink",
	}

static var COLORS_PRETTY_V1 = {
	"SRC": "aquamarine",
	"ADDONS": "peru",
	"TEST": "green_yellow",
	",": "crimson",
	"(": "crimson",
	")": "crimson",
	"[": "crimson",
	"]": "crimson",
	"{": "crimson",
	"}": "crimson",
	"&": "coral",
	"^": "coral",
	"dict_key": "cadet_blue",
	"vector_value": "cornflower_blue",
	"class_name": "cadet_blue",
	TYPE_NIL: "coral",
	TYPE_BOOL: "pink",
	TYPE_INT: "cornflower_blue",
	TYPE_FLOAT: "cornflower_blue",
	TYPE_STRING: "dark_gray",
	TYPE_VECTOR2: "cornflower_blue",
	TYPE_VECTOR2I: "cornflower_blue",
	TYPE_RECT2: "cornflower_blue",
	TYPE_RECT2I: "cornflower_blue",
	TYPE_VECTOR3: "cornflower_blue",
	TYPE_VECTOR3I: "cornflower_blue",
	TYPE_TRANSFORM2D: "pink",
	TYPE_VECTOR4: "cornflower_blue",
	TYPE_VECTOR4I: "cornflower_blue",
	TYPE_PLANE: "pink",
	TYPE_QUATERNION: "pink",
	TYPE_AABB: "pink",
	TYPE_BASIS: "pink",
	TYPE_TRANSFORM3D: "pink",
	TYPE_PROJECTION: "pink",
	TYPE_COLOR: "pink",
	TYPE_STRING_NAME: "pink",
	TYPE_NODE_PATH: "pink",
	TYPE_RID: "pink",
	TYPE_OBJECT: "pink",
	TYPE_CALLABLE: "pink",
	TYPE_SIGNAL: "pink",
	TYPE_DICTIONARY: "pink",
	TYPE_ARRAY: "pink",
	TYPE_PACKED_BYTE_ARRAY: "pink",
	TYPE_PACKED_INT32_ARRAY: "pink",
	TYPE_PACKED_INT64_ARRAY: "pink",
	TYPE_PACKED_FLOAT32_ARRAY: "pink",
	TYPE_PACKED_FLOAT64_ARRAY: "pink",
	TYPE_PACKED_STRING_ARRAY: "pink",
	TYPE_PACKED_VECTOR2_ARRAY: "pink",
	TYPE_PACKED_VECTOR3_ARRAY: "pink",
	TYPE_PACKED_COLOR_ARRAY: "pink",
	TYPE_MAX: "pink",
	}

## set color scheme ####################################

static func set_colors_termsafe():
	set_color_scheme(Log.COLORS_TERMINAL_SAFE)

static func set_colors_pretty():
	set_color_scheme(Log.COLORS_PRETTY_V1)

static func color_scheme(opts={}):
	var scheme = opts.get("color_scheme", {})
	# fill in any missing vals with the set scheme, then the term-safe fallbacks
	scheme.merge(Log.get_config_color_scheme())
	scheme.merge(Log.COLORS_TERMINAL_SAFE)
	return scheme

static func color_wrap(s, opts={}):
	var use_color = opts.get("use_color", true)
	# don't rebuild the color scheme every time
	var colors = opts.get("built_color_scheme", color_scheme(opts))

	if use_color:
		var color = opts.get("color")
		if not color:
			var s_type = opts.get("typeof", typeof(s))
			if s_type is String:
				# type overwrites
				color = colors.get(s_type)
			elif s_type is int and s_type == TYPE_STRING:
				# specific strings/punctuation
				var s_trimmed = s.strip_edges()
				if s_trimmed in colors:
					color = colors.get(s_trimmed)
				else:
					# fallback string color
					color = colors.get(s_type)
			else:
				# all other types
				color = colors.get(s_type)

		if color == null:
			print("Log.gd could not determine color for object: %s type: (%s)" % [str(s), typeof(s)])

		return "[color=%s]%s[/color]" % [color, s]
	else:
		return s

## overwrites ###########################################################################

static var log_overwrites = {
	"Vector2": func(msg, opts):
		if opts.get("use_color", true):
			return '%s%s%s%s%s' % [
				Log.color_wrap("(", opts),
				Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(",", opts),
				Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(")", opts),
				]
		else:
			return '(%s,%s)' % [msg.x, msg.y],
	}

static func register_overwrite(key, handler):
	# TODO warning on key exists?
	# support multiple handlers?
	# return success/fail?
	# validate the key/handler somehow?
	log_overwrites[key] = handler

## to_pretty ###########################################################################

# returns the passed object as a decorated string
static func to_pretty(msg, opts={}):
	var newlines = opts.get("newlines", false)
	var use_color = opts.get("use_color", true)
	var indent_level = opts.get("indent_level", 0)
	if not "indent_level" in opts:
		opts["indent_level"] = indent_level

	var color_scheme = opts.get("built_color_scheme", color_scheme(opts))
	if not "built_color_scheme" in opts:
		opts["built_color_scheme"] = color_scheme

	if not is_instance_valid(msg) and typeof(msg) == TYPE_OBJECT:
		return str("invalid instance: ", msg)

	if msg == null:
		return Log.color_wrap(msg, opts)

	if msg is Object and msg.get_class() in log_overwrites:
		return log_overwrites.get(msg.get_class()).call(msg, opts)
	elif typeof(msg) in log_overwrites:
		return log_overwrites.get(typeof(msg)).call(msg, opts)

	# objects
	if msg is Object and msg.has_method("to_pretty"):
		return Log.to_pretty(msg.to_pretty(), opts)
	if msg is Object and msg.has_method("data"):
		return Log.to_pretty(msg.data(), opts)
	if msg is Object and msg.has_method("to_printable"):
		return Log.to_pretty(msg.to_printable(), opts)

	# arrays
	if msg is Array or msg is PackedStringArray:
		if len(msg) > Log.get_max_array_size():
			pr("[DEBUG]: truncating large array. total:", len(msg))
			msg = msg.slice(0, Log.get_max_array_size() - 1)
			if newlines:
				msg.append("...")

		var tmp = Log.color_wrap("[ ", opts)
		var last = len(msg) - 1
		for i in range(len(msg)):
			if newlines and last > 1:
				tmp += "\n\t"
			tmp += Log.to_pretty(msg[i],
				# duplicate here to prevent indenting-per-msg
				# e.g. when printing an array of dictionaries
				opts.duplicate(true))
			if i != last:
				tmp += Log.color_wrap(", ", opts)
		tmp += Log.color_wrap(" ]", opts)
		return tmp

	# dictionary
	elif msg is Dictionary:
		var tmp = Log.color_wrap("{ ", opts)
		var ct = len(msg)
		var last
		if len(msg) > 0:
			last = msg.keys()[-1]
		for k in msg.keys():
			var val
			if k in Log.get_dictionary_skip_keys():
				val = "..."
			else:
				opts.indent_level += 1
				val = Log.to_pretty(msg[k], opts)
			if newlines and ct > 1:
				tmp += "\n\t" \
					+ range(indent_level)\
					.map(func(_i): return "\t")\
						.reduce(func(a, b): return str(a, b), "")
			if use_color:
				var key = Log.color_wrap('"%s"' % k, Log.assoc(opts, "typeof", "dict_key"))
				tmp += "%s: %s" % [key, val]
			else:
				tmp += '"%s": %s' % [k, val]
			if last and str(k) != str(last):
				tmp += Log.color_wrap(", ", opts)
		tmp += Log.color_wrap(" }", opts)
		return tmp

	# strings
	elif msg is String:
		if msg == "":
			return '""'
		if "[color=" in msg and "[/color]" in msg:
			# assumes the string is already colorized
			# NOT PERFECT! could use a regex for something more robust
			return msg
		return Log.color_wrap(msg, opts)
	elif msg is StringName:
		return str(Log.color_wrap("&", opts), '"%s"' % msg)
	elif msg is NodePath:
		return str(Log.color_wrap("^", opts), '"%s"' % msg)

	# vectors
	elif msg is Vector2 or msg is Vector2i:
		return log_overwrites.get("Vector2").call(msg, opts)

	elif msg is Vector3 or msg is Vector3i:
		if use_color:
			return '%s%s%s%s%s%s%s' % [
				Log.color_wrap("(", opts),
				Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(",", opts),
				Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(",", opts),
				Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(")", opts),
				]
		else:
			return '(%s,%s,%s)' % [msg.x, msg.y, msg.z]
	elif msg is Vector4 or msg is Vector4i:
		if use_color:
			return '%s%s%s%s%s%s%s%s%s' % [
				Log.color_wrap("(", opts),
				Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(",", opts),
				Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(",", opts),
				Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(",", opts),
				Log.color_wrap(msg.w, Log.assoc(opts, "typeof", "vector_value")),
				Log.color_wrap(")", opts),
				]
		else:
			return '(%s,%s,%s,%s)' % [msg.x, msg.y, msg.z, msg.w]

	# packed scene
	elif msg is PackedScene:
		if msg.resource_path != "":
			return str(Log.color_wrap("PackedScene:", opts), '%s' % msg.resource_path.get_file())
		elif msg.get_script() != null and msg.get_script().resource_path != "":
			return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name"))
		else:
			return Log.color_wrap(msg, opts)

	# resource
	elif msg is Resource:
		if msg.get_script() != null and msg.get_script().resource_path != "":
			return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name"))
		elif msg.resource_path != "":
			return str(Log.color_wrap("Resource:", opts), '%s' % msg.resource_path.get_file())
		else:
			return Log.color_wrap(msg, opts)

	# refcounted
	elif msg is RefCounted:
		if msg.get_script() != null and msg.get_script().resource_path != "":
			return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name"))
		else:
			return Log.color_wrap(msg.get_class(), Log.assoc(opts, "typeof", "class_name"))

	# fallback to primitive-type lookup
	else:
		return Log.color_wrap(msg, opts)

## to_printable ###########################################################################

static func log_prefix(stack):
	if len(stack) > 1:
		var call_site = stack[1]
		var basename = call_site["source"].get_file().get_basename()
		var line_num = str(call_site.get("line", 0))
		if call_site["source"].match("*/test/*"):
			return "{" + basename + ":" + line_num + "}: "
		elif call_site["source"].match("*/addons/*"):
			return "<" + basename + ":" + line_num + ">: "
		else:
			return "[" + basename + ":" + line_num + "]: "

static func to_printable(msgs, opts={}):
	var stack = opts.get("stack", [])
	var pretty = opts.get("pretty", true)
	var newlines = opts.get("newlines", false)
	var m = ""
	if len(stack) > 0:
		var prefix = Log.log_prefix(stack)
		var prefix_type
		if prefix != null and prefix[0] == "[":
			prefix_type = "SRC"
		elif prefix != null and prefix[0] == "{":
			prefix_type = "TEST"
		elif prefix != null and prefix[0] == "<":
			prefix_type = "ADDONS"
		if pretty:
			m += Log.color_wrap(prefix, Log.assoc(opts, "typeof", prefix_type))
		else:
			m += prefix
	for msg in msgs:
		# add a space between msgs
		if pretty:
			m += "%s " % Log.to_pretty(msg, opts)
		else:
			m += "%s " % str(msg)
	return m.trim_suffix(" ")

## public print fns ###########################################################################

static func is_not_default(v):
	return not v is String or (v is String and v != "ZZZDEF")

static func pr(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
	var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
	msgs = msgs.filter(Log.is_not_default)
	var m = Log.to_printable(msgs, {stack=get_stack()})
	print_rich(m)

static func info(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
	var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
	msgs = msgs.filter(Log.is_not_default)
	var m = Log.to_printable(msgs, {stack=get_stack()})
	print_rich(m)

static func log(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
	var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
	msgs = msgs.filter(Log.is_not_default)
	var m = Log.to_printable(msgs, {stack=get_stack()})
	print_rich(m)

static func prn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
	var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
	msgs = msgs.filter(Log.is_not_default)
	var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true})
	print_rich(m)

static func warn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
	var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
	msgs = msgs.filter(Log.is_not_default)
	var rich_msgs = msgs.duplicate()
	rich_msgs.push_front("[color=yellow][WARN][/color]")
	print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true}))
	var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false})
	push_warning(m)

static func err(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
	var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
	msgs = msgs.filter(Log.is_not_default)
	var rich_msgs = msgs.duplicate()
	rich_msgs.push_front("[color=red][ERR][/color]")
	print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true}))
	var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false})
	push_error(m)

static func error(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
	var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
	msgs = msgs.filter(Log.is_not_default)
	var rich_msgs = msgs.duplicate()
	rich_msgs.push_front("[color=red][ERR][/color]")
	print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true}))
	var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false})
	push_error(m)
