extends Tween

var loop = 1

signal finished_animation

var _animation_data = []

enum PLAY_MODE {
	NORMAL,
	BACKWARDS
}

# Needed to use interpolate_property
var _fake_property: Dictionary = {}
var _callbacks := {}

func _ready():
	connect("tween_started", self, '_on_tween_started')
	connect("tween_step", self, '_on_tween_step_with_easing')
	connect("tween_step", self, '_on_tween_step_with_easing_callback')
	connect("tween_step", self, '_on_tween_step_with_easing_funcref')
	connect("tween_step", self, '_on_tween_step_without_easing')
	connect("tween_completed", self, '_on_tween_completed')
	

func play(node, animation_name, duration):
	var script = DialogicAnimaResources.get_animation_script(animation_name.strip_edges())

	if not script:
		printerr('animation not found: %s' % animation_name)

		return duration
	
	var real_duration = script.generate_animation(self, {'node':node, 'duration':duration, 'wait_time':0})
	
	if real_duration is float:
		duration = real_duration
	
	var index := 0
	
	for animation_data in _animation_data:
		var easing_points

		if animation_data.has('easing'):
			if animation_data.easing is FuncRef:
				easing_points = animation_data.easing
			else:
				easing_points = get_easing_points(animation_data.easing)

		if animation_data.has('easing_points'):
			easing_points = animation_data.easing_points

		animation_data._easing_points = easing_points

		animation_data._animation_callback = funcref(self, '_calculate_from_and_to')

		if easing_points is Array:
			animation_data._use_step_callback = '_on_tween_step_with_easing'
		elif easing_points is String:
			animation_data._use_step_callback = '_on_tween_step_with_easing_callback'
		elif easing_points is FuncRef:
			animation_data._use_step_callback = '_on_tween_step_with_easing_funcref'
		else:
			animation_data._use_step_callback = '_on_tween_step_without_easing'

		index += 1

	var started := start()

	if not started:
		printerr('something went wrong while trying to start the tween')
	
	if is_connected("tween_all_completed", self, 'finished_once'): disconnect("tween_all_completed", self, 'finished_once')
	connect('tween_all_completed', self, 'finished_once', [node, animation_name, duration])

func finished_once(node, animation, duration):
	loop -= 1
	if loop > 0:
		play(node, animation, duration)
	else:
		emit_signal('finished_animation')

# Given an array of frames generates the animation data using relative end value
#
# frames = [{
#	percentage = the percentage of the animation
#	to = the relative end value
#	easing_points = the easing points for the bezier curver (optional)
# }]
#
func add_relative_frames(data: Dictionary, property: String, frames: Array) -> float:
	return _add_frames(data, property, frames, true)

#
# Given an array of frames generates the animation data using absolute end value
#
# frames = [{
#	percentage = the percentage of the animation
#	to = the relative end value
#	easing_points = the easing points for the bezier curver (optional)
# }]
#
func add_frames(data: Dictionary, property: String, frames: Array) -> float:
	return _add_frames(data, property, frames)


func _add_frames(data: Dictionary, property: String, frames: Array, relative: bool = false) -> float:
	var duration: float = data.duration if data.has('duration') else 0.0
	var _wait_time: float = data._wait_time if data.has('_wait_time') else 0.0
	var last_duration := 0.0

	var keys_to_ignore = ['duration', '_wait_time']
	for frame in frames:
		var percentage = frame.percentage if frame.has('percentage') else 100.0
		percentage /= 100.0

		var frame_duration = max(0.000001, duration * percentage)
		var diff = frame_duration - last_duration
		var is_first_frame = true
		var is_last_frame = percentage == 1

		var animation_data = {
			property = property,
			relative = relative,
			duration = diff,
			_wait_time = _wait_time
		}

		# We need to restore the animation just before the node is animated
		# but we also need to consider that a node can have multiple
		# properties animated, so we need to restore it only before the first
		# animation starts
		for animation in _animation_data:
			if animation.node == data.node:
				is_first_frame = false

				if animation.has('_is_last_frame'):
					is_last_frame = false

		if is_first_frame:
			animation_data._is_first_frame = true

		if is_last_frame:
			animation_data._is_last_frame = true

		for key in frame:
			if key != 'percentage':
				animation_data[key] = frame[key]

		for key in data:
			if key == 'callback' and percentage < 1:
				animation_data.erase(key)
			elif keys_to_ignore.find(key) < 0:
				animation_data[key] = data[key]

		add_animation_data(animation_data)

		last_duration = frame_duration
		_wait_time += diff

	return _wait_time


func add_animation_data(animation_data: Dictionary, play_mode: int = PLAY_MODE.NORMAL) -> void:
	_animation_data.push_back(animation_data)

	var index = str(_animation_data.size())
	var duration = animation_data.duration if animation_data.has('duration') else 1
	var property_key = 'p' + index
	
	_fake_property[property_key] = 0.0

	if animation_data.has('on_completed') and animation_data.has('_is_last_frame'):
		_callbacks[property_key] = animation_data.on_completed

	var from := 0.0 if play_mode == PLAY_MODE.NORMAL else 1.0
	var to := 1.0 - from

	interpolate_property(
		self,
		'_fake_property:' + property_key,
		from,
		to,
		duration,
		Tween.TRANS_LINEAR,
		Tween.EASE_IN_OUT,
		animation_data._wait_time
	)


func _on_tween_step_with_easing(object: Object, key: NodePath, _time: float, elapsed: float):
	var index := _get_animation_data_index(key)

	if _animation_data[index]._use_step_callback != '_on_tween_step_with_easing':
		return

	var easing_points = _animation_data[index]._easing_points
	var p1 = easing_points[0]
	var p2 = easing_points[1]
	var p3 = easing_points[2]
	var p4 = easing_points[3]

	var easing_elapsed = _cubic_bezier(Vector2.ZERO, Vector2(p1, p2), Vector2(p3, p4), Vector2(1, 1), elapsed)

	_animation_data[index]._animation_callback.call_func(index, easing_elapsed)

func _on_tween_step_with_easing_callback(object: Object, key: NodePath, _time: float, elapsed: float):
	var index := _get_animation_data_index(key)

	if _animation_data[index]._use_step_callback != '_on_tween_step_with_easing_callback':
		return

	var easing_points_function = _animation_data[index]._easing_points
	var easing_callback = funcref(self, easing_points_function)
	var easing_elapsed = easing_callback.call_func(elapsed)

	_animation_data[index]._animation_callback.call_func(index, easing_elapsed)

func _on_tween_step_with_easing_funcref(object: Object, key: NodePath, _time: float, elapsed: float):
	var index := _get_animation_data_index(key)

	if _animation_data[index]._use_step_callback != '_on_tween_step_with_easing_funcref':
		return

	var easing_callback = _animation_data[index]._easing_points
	var easing_elapsed = easing_callback.call_func(elapsed)

	_animation_data[index]._animation_callback.call_func(index, easing_elapsed)

func _on_tween_step_without_easing(object: Object, key: NodePath, _time: float, elapsed: float):
	var index := _get_animation_data_index(key)

	if _animation_data[index]._use_step_callback != '_on_tween_step_without_easing':
		return

	_animation_data[index]._animation_callback.call_func(index, elapsed)

func _get_animation_data_index(key: NodePath) -> int:
	var s = str(key)

	return int(s.replace('_fake_property:p', '')) - 1

func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float) -> float:
	var q0 = p0.linear_interpolate(p1, t)
	var q1 = p1.linear_interpolate(p2, t)
	var q2 = p2.linear_interpolate(p3, t)

	var r0 = q0.linear_interpolate(q1, t)
	var r1 = q1.linear_interpolate(q2, t)

	var s = r0.linear_interpolate(r1, t)

	return s.y

func _calculate_from_and_to(index: int, value: float) -> void:
	var animation_data = _animation_data[index]
	var node = animation_data.node

	var do_calculate = true

	if animation_data.has('_recalculate_from_to') and not animation_data._recalculate_from_to and animation_data.has('_property_data'):
		do_calculate = false

	if do_calculate:
		_do_calculate_from_to(node, animation_data)

	if animation_data._property_data.has('subkey'):
		animation_data._animation_callback = funcref(self, '_on_animation_with_subkey')
	elif animation_data._property_data.has('key'):
		animation_data._animation_callback = funcref(self, '_on_animation_with_key')
	else:
		animation_data._animation_callback = funcref(self, '_on_animation_without_key')

	_animation_data[index]._animation_callback.call_func(index, value)

func _do_calculate_from_to(node: Node, animation_data: Dictionary) -> void:
	var from
	var to
	var relative = animation_data.relative if animation_data.has('relative') else false
	var node_from = DialogicAnimaPropertiesHelper.get_property_initial_value(node, animation_data.property)

	if animation_data.has('from'):
		from = _maybe_convert_from_deg_to_rad(node, animation_data, animation_data.from)
		from = _maybe_calculate_relative_value(relative, from, node_from)
	else:
		from = node_from
		animation_data.__from = from

	if animation_data.has('to'):
		to = _maybe_convert_from_deg_to_rad(node, animation_data, animation_data.to)
		to = _maybe_calculate_relative_value(relative, to, from)
	else:
		to = from
		animation_data.__to = to

	if animation_data.has('pivot'):
		if node is Spatial:
			printerr('3D Pivot not supported yet')
		else:
			DialogicAnimaPropertiesHelper.set_2D_pivot(animation_data.node, animation_data.pivot)

	animation_data._property_data = DialogicAnimaPropertiesHelper.map_property_to_godot_property(node, animation_data.property)
	
	animation_data._property_data.diff = to - from
	animation_data._property_data.from = from

func _maybe_calculate_relative_value(relative, value, current_node_value):
	if not relative:
		return value

	return value + current_node_value

func _maybe_convert_from_deg_to_rad(node: Node, animation_data: Dictionary, value):
	if not node is Spatial or animation_data.property.find('rotation') < 0:
		return value

	if value is Vector3:
		return Vector3(deg2rad(value.x), deg2rad(value.y), deg2rad(value.z))

	return deg2rad(value)

func _on_animation_with_key(index: int, elapsed: float) -> void:
	var animation_data = _animation_data[index]
	var property_data = _animation_data[index]._property_data
	var node = animation_data.node
	var value = property_data.from + (property_data.diff * elapsed)
	
	if is_instance_valid(node):
		node[property_data.property_name][property_data.key] = value

func _on_animation_with_subkey(index: int, elapsed: float) -> void:
	var animation_data = _animation_data[index]
	var property_data = _animation_data[index]._property_data
	var node = animation_data.node
	var value = property_data.from + (property_data.diff * elapsed)
	
	if is_instance_valid(node):
		node[property_data.property_name][property_data.key][property_data.subkey] = value

func _on_animation_without_key(index: int, elapsed: float) -> void:
	var animation_data = _animation_data[index]
	var property_data = _animation_data[index]._property_data
	var node = animation_data.node
	var value = property_data.from + (property_data.diff * elapsed)

	if property_data.has('callback'):
		property_data.callback.call_func(property_data.param, value)

		return
	
	if is_instance_valid(node):
		node[property_data.property_name] = value

#
# We don't want the user to specify the from/to value as color
# we animate opacity.
# So this function converts the "from = #" -> Color(.., .., .., #)
# same for the to value
#
func _maybe_adjust_modulate_value(animation_data: Dictionary, value):
	var property = animation_data.property
	var node = animation_data.node

	if not property == 'opacity':
		return value

	if value is int or value is float:
		var color = node.modulate

		color.a = value

		return color

	return value

func _on_tween_completed(_ignore, property_name: String) -> void:
	var property_key = property_name.replace(':_fake_property:', '')

	if _callbacks.has(property_key):
		var callback = _callbacks[property_key]

		if not callback is Array or callback.size() == 1:
			callback[0].call_func()
		else:
			callback[0].call_funcv(callback[1])

func _on_tween_started(_ignore, key) -> void:
	var index := _get_animation_data_index(key)
	#var hide_strategy = _visibility_strategy
	var animation_data = _animation_data[index]

#	if animation_data.has('hide_strategy'):
#		hide_strategy = animation_data.hide_strategy

	var node = animation_data.node
	var should_restore_visibility := false
	var should_restore_modulate := false

#	if hide_strategy == Anima.VISIBILITY.HIDDEN_ONLY:
#		should_restore_visibility = true
#	elif hide_strategy == Anima.VISIBILITY.HIDDEN_AND_TRANSPARENT:
#		should_restore_modulate = true
#		should_restore_visibility = true
#	elif hide_strategy == Anima.VISIBILITY.TRANSPARENT_ONLY:
#		should_restore_modulate = true

	if should_restore_modulate:
		var old_modulate = node.get_meta('_old_modulate')

		if old_modulate:
			node.modulate = old_modulate

	if should_restore_visibility:
		node.show()

	var should_trigger_on_started: bool = animation_data.has('_is_first_frame') and animation_data._is_first_frame and animation_data.has('on_started')
	if should_trigger_on_started:
		var fn: FuncRef
		var args: Array = []
		if animation_data.on_started is Array:
			fn = animation_data.on_started[0]
			args = animation_data.on_started.slice(1, -1)
		else:
			fn = animation_data.on_started

		fn.call_funcv(args)


####################################################################################################
####################################################################################################
##										FROM ANIMA EASING
####################################################################################################
####################################################################################################

enum EASING {
	LINEAR,   
	EASE,
	EASE_IN_OUT,
	EASE_IN,
	EASE_OUT,
	EASE_IN_SINE,
	EASE_OUT_SINE,
	EASE_IN_OUT_SINE,
	EASE_IN_QUAD,
	EASE_OUT_QUAD,
	EASE_IN_OUT_QUAD,
	EASE_IN_CUBIC,
	EASE_OUT_CUBIC,
	EASE_IN_OUT_CUBIC,
	EASE_IN_QUART,
	EASE_OUT_QUART,
	EASE_IN_OUT_QUART,
	EASE_IN_QUINT,
	EASE_OUT_QUINT,
	EASE_IN_OUT_QUINT,
	EASE_IN_EXPO,
	EASE_OUT_EXPO,
	EASE_IN_OUT_EXPO,
	EASE_IN_CIRC,
	EASE_OUT_CIRC,
	EASE_IN_OUT_CIRC,
	EASE_IN_BACK,
	EASE_OUT_BACK,
	EASE_IN_OUT_BACK,
	EASE_IN_ELASTIC,
	EASE_OUT_ELASTIC,
	EASE_IN_OUT_ELASTIC,
	EASE_IN_BOUNCE,
	EASE_OUT_BOUNCE,
	EASE_IN_OUT_BOUNCE,
}

const _easing_mapping = {
	EASING.LINEAR: null,
	EASING.EASE: [0.25, 0.1, 0.25, 1],
	EASING.EASE_IN_OUT: [0.42, 0, 0.58, 1],
	EASING.EASE_IN: [0.42, 0, 1, 1],
	EASING.EASE_OUT: [0, 0, 0.58, 1],
	EASING.EASE_IN_SINE: [0, 0, 1, .5],
	EASING.EASE_OUT_SINE: [0.61, 1, 0.88, 1],
	EASING.EASE_IN_OUT_SINE: [0.37, 0, 0.63, 1],
	EASING.EASE_IN_QUAD: [0.11, 0, 0.5, 0],
	EASING.EASE_OUT_QUAD: [0.5, 1.0, 0.89, 1],
	EASING.EASE_IN_OUT_QUAD: [0.45, 0, 0.55, 1],
	EASING.EASE_IN_CUBIC: [0.32, 0, 0.67, 0],
	EASING.EASE_OUT_CUBIC: [0.33, 1, 0.68, 1],
	EASING.EASE_IN_OUT_CUBIC: [0.65, 0, 0.35, 1],
	EASING.EASE_IN_QUART: [0.5, 0, 0.75, 0],
	EASING.EASE_OUT_QUART: [0.25, 1, 0.5, 1],
	EASING.EASE_IN_OUT_QUART: [0.76, 0, 0.24, 1],
	EASING.EASE_IN_QUINT: [0.64, 0, 0.78, 0],
	EASING.EASE_OUT_QUINT: [0.22, 1, 0.36, 1],
	EASING.EASE_IN_OUT_QUINT: [0.83, 0, 0.17, 1],
	EASING.EASE_IN_EXPO: [0.7, 0, 0.84, 0],
	EASING.EASE_OUT_EXPO: [0.16, 1, 0.3, 1],
	EASING.EASE_IN_OUT_EXPO: [0.87, 0, 0.13, 1],
	EASING.EASE_IN_CIRC: [0.55, 0, 0.1, 0.45],
	EASING.EASE_OUT_CIRC: [0, 0.55, 0.45, 1],
	EASING.EASE_IN_OUT_CIRC: [0.85, 0, 0.15, 1],
	EASING.EASE_IN_BACK: [0.36, 0, 0.66, -0.56],
	EASING.EASE_OUT_BACK: [0.36, 1.56, 0.64, 1],
	EASING.EASE_IN_OUT_BACK: [0.68, -0.6, 0.32, 1.6],
	EASING.EASE_IN_ELASTIC: 'ease_in_elastic',
	EASING.EASE_OUT_ELASTIC: 'ease_out_elastic',
	EASING.EASE_IN_OUT_ELASTIC: 'ease_in_out_elastic',
	EASING.EASE_IN_BOUNCE: 'ease_in_bounce',
	EASING.EASE_OUT_BOUNCE: 'ease_out_bounce',
	EASING.EASE_IN_OUT_BOUNCE: 'ease_in_out_bounce'
}

const _ELASTIC_C4: float = (2.0 * PI) / 3.0
const _ELASTIC_C5: float = (2.0 * PI) / 4.5

static func get_easing_points(easing_name):
	if _easing_mapping.has(easing_name):
		return _easing_mapping[easing_name]

	printerr('Easing not found: ' + str(easing_name))

	return _easing_mapping[EASING.LINEAR]

static func ease_in_elastic(elapsed: float) -> float:
	if elapsed == 0:
		return 0.0
	elif elapsed == 1:
		return 1.0

	return -pow(2, 10 * elapsed - 10) * sin((elapsed * 10 - 10.75) * _ELASTIC_C4)

static func ease_out_elastic(elapsed: float) -> float:
	if elapsed == 0:
		return 0.0
	elif elapsed == 1:
		return 1.0

	return pow(2, -10 * elapsed) * sin((elapsed * 10 - 0.75) * _ELASTIC_C4) + 1

static func ease_in_out_elastic(elapsed: float) -> float:
	if elapsed == 0:
		return 0.0
	elif elapsed == 1:
		return 1.0
	elif elapsed < 0.5:
		return -(pow(2, 20 * elapsed - 10) * sin((20 * elapsed - 11.125) * _ELASTIC_C5)) / 2

	return (pow(2, -20 * elapsed + 10) * sin((20 * elapsed - 11.125) * _ELASTIC_C5)) / 2 + 1

const n1 = 7.5625;
const d1 = 2.75;

static func ease_in_bounce(elapsed: float) -> float:
	return 1 - ease_out_bounce(1.0 - elapsed)

static func ease_out_bounce(elapsed: float) -> float:
	if elapsed < 1 / d1:
		return n1 * elapsed * elapsed;
	elif elapsed < 2 / d1:
		elapsed -= 1.5 / d1

		return n1 * elapsed * elapsed + 0.75;
	elif elapsed < 2.5 / d1:
		elapsed -= 2.25 / d1

		return n1 * elapsed * elapsed + 0.9375;

	elapsed -= 2.625 / d1
	return n1 * elapsed * elapsed + 0.984375;

static func ease_in_out_bounce(elapsed: float) -> float:
	if elapsed < 0.5:
		return (1 - ease_out_bounce(1 - 2 * elapsed)) / 2
	
	return (1 + ease_out_bounce(2 * elapsed - 1)) / 2
