extends Actor
const PhysicsFunc = preload("res://src/Utilities/Physic/PhysicsFunc.gd")

onready var players = get_tree().get_nodes_in_group("player")

onready var vision_raycast: RayCast2D = $VisionRayCast
onready var orientation: RayCast2D = $Orientation
onready var feeler_raycast: RayCast2D = $FeelerRayCast
onready var tilemap: TileMap = $"../%TileMap"
onready var state_machine = $Statemachine
onready var jump_timer: Timer
onready var target_lost_timer: Timer
onready var rng = RandomNumberGenerator.new()
onready var invincible_shader = preload("res://src/Actors/Blobby/InvincibleShader.tres")

# Is given in blocks
export var frog_number := 0
export var vision_distance := 6.0
export var attack_jump_range := 6.0
export var aggressive_to_player := false
export var loose_target_seconds := 3.0
# Jump distance in blocks
export var default_jump_distance := 4.0
export var default_jump_angle := 70.0
export var jump_time_search := 0.7
export var jump_time_hunt := 0.3
export var jump_time_standard_deviation := 0.1
# TODO Make constant for project
export var block_size := 16

# Also in blocks
var movement_radius: float
var anchor: Node2D
var is_bound := false
var was_restricted := false
var barely_held_back_counter := 0
var has_reversed := false

var food_sources = []
var target: Object = null
var food_target: Object = null

var start_x := 0.0
var in_air := false
var is_hurt := false
var stored_x_vel = 0.0

var current_delta = 0.0
var detect_timer := 0.0

var reversing_possible_searching := true

var attached_player = null


func _ready():
	default_jump_distance = default_jump_distance * tilemap.cell_size.x
	jump_timer = Timer.new()
	jump_timer.set_one_shot(true)
	jump_timer.connect("timeout", self, "jump")
	target_lost_timer = Timer.new()
	target_lost_timer.set_one_shot(true)
	target_lost_timer.connect("timeout", self, "loose_target")
	add_child(jump_timer)
	add_child(target_lost_timer)
	# TODO this is so bad ;_;
	if get_parent().name.begins_with("Bound"):
		is_bound = true
	else:
		level_state.free_a_frog(frog_number)
	level_state.register_frog(frog_number, !is_bound)
	# TODO Stays harmless for now
	#if(is_bound): add_to_group("harmful")


func bind_to_anchor(anchor_node: Node2D, radius: float) -> void:
	anchor = anchor_node
	movement_radius = radius * block_size
	is_bound = true
	# TODO multiple free frogs
	$Digit.visible = true
	$Digit.frame = frog_number
	$LeashAnchor.visible = is_bound


func execute_movement(delta: float) -> void:
	# Navigation2DServer.map_get_path()
	current_delta = delta
	# TODO what when the game runs really long and the float runs out of space?
	# Achievment maybe lul
	detect_timer += delta
	velocity.y += _gravity * delta
	if is_bound:
		var next_position = global_position + velocity * current_delta
		var current_distance = global_position.distance_to(anchor.global_position)
		var new_distance = next_position.distance_to(anchor.global_position)
		# TODO Fix this in respects to x and y distances and movement dampening
		# Maybe use mathemathematics or something idfc
		if current_distance >= movement_radius && new_distance > current_distance:
			velocity.x = velocity.x * 0.8
			velocity.y = velocity.y * 0.8
			was_restricted = true
	velocity = move_and_slide(velocity, FLOOR_NORMAL, false, 4, 0.785398, false)

	if $"%GroundDetector".get_overlapping_bodies().size() > 0:
		velocity.y -= 10 * (delta / 0.0083)
		var min_x_slide_velocity = 50 * (delta / 0.0083)
		velocity.x = sign(velocity.x) * max(min_x_slide_velocity, velocity.x * 0.99)
		return
	elif is_on_floor():
		velocity = Vector2(0, 0)

	# Reverse direction when hitting limit


func die() -> void:
	queue_free()


func _on_EnemySkin_area_entered(area: Area2D) -> void:
	if area.is_in_group("harmful") && !area.is_in_group("frogfood"):
		get_node("EnemyBody").disabled = true
		GlobalAudio.play_scene_independent("res://assets/sounds/falling.wav", "Effects", -18)
		die()


func _on_EnemySkin_body_entered(body: Node) -> void:
	if body.is_in_group("frogfood"):
		loose_target()
		$SceneAudio.play_sound("res://assets/sounds/TerraZoo_Mammal_RingTailedLemur_EatingCelery_KMR81i.wav", -16.5)
		body.die()


func _on_StompDetector_body_entered(body: Node) -> void:
	if body.is_in_group("player"):
		attached_player = body
		$FeelerRayCast.collision_mask -= 1
	if !body.is_in_group("player") || is_hurt || !$StompTimeout.is_stopped():
		return
	var incoming_vel_vector: Vector2 = body.velocity.normalized()
	signal_manager.emit_signal("got_stomped")
	remove_from_group("harmful")
	# TODO Weakpoint group is not needed per se
	$StompDetector.remove_from_group("weakpoint")
	get_node("EnemyBody").disabled = true
	is_hurt = true
	$FrogSprite.material = invincible_shader
	$HurtTimer.start()


func _on_StompDetector_body_exited(body: Node) -> void:
	if attached_player == body:
		$FeelerRayCast.collision_mask += 1
		$StompTimeout.start()
		attached_player = null


func searching() -> Vector2:
	if detect_timer > 0.333:
		search_next_target()
		detect_timer = 0.0
	if is_on_floor():
		if jump_timer.is_stopped():
			jump_timer.start(rng.randfn(jump_time_search, jump_time_standard_deviation))
		if in_air:
			in_air = false
	else:
		if !in_air:
			start_x = global_position.x
		reversing_possible_searching = true
		jump_timer.stop()
		in_air = true
	return velocity


func search_next_target():
	if target != null && !weakref(target).get_ref():
		return
	detect_food()
	if food_target == null && is_bound && aggressive_to_player:
		detect_player()


func hunting() -> Vector2:
	var was_target_freed = !weakref(target).get_ref()
	if detect_timer > 0.333:
		search_next_target()
		detect_timer = 0.0
	#TODO Dependent on block size
	elif (
		is_on_floor()
		&& food_target != null
		&& !was_target_freed
		&& (
			global_position.distance_to(food_target.global_position)
			<= attack_jump_range * block_size
		)
	):
		var collider = check_feeler(food_target.global_position - global_position)
		if !was_restricted && collider != null && collider.is_in_group("frogfood"):
			jump_timer.stop()
			return attack_jump(food_target.global_position)

	if is_on_floor():
		if jump_timer.is_stopped():
			jump_timer.start(rng.randfn(jump_time_hunt, jump_time_standard_deviation))
		if in_air:
			in_air = false

	else:
		if !in_air:
			start_x = global_position.x
		reversing_possible_searching = true
		jump_timer.stop()
		in_air = true

	if barely_held_back_counter > 1:
		barely_held_back_counter = 0
		loose_target()

	if (
		target != null
		&& !was_target_freed
		&& sign((target.global_position - global_position).x) != get_facing_direction()
	):
		# TODO Waits in front of too small tunnels if it sees the target on the other side
		# It's ok behavior for now
		reverse_facing_direction()

	return velocity


func detect_food() -> void:
	# TODO What if food spawns in
	food_sources = get_tree().get_nodes_in_group("frogfood")
	if food_sources.empty():
		return

	var i = 0
	var min_dist_f_index = 0
	var min_dist = (food_sources[0].global_position - global_position).length()
	var food_node = null
	for f in food_sources:
		var new_dist = (food_sources[i].global_position - global_position).length()
		min_dist = new_dist if new_dist < min_dist else min_dist
		min_dist_f_index = i if new_dist < min_dist else min_dist_f_index
		i += 1
	food_node = food_sources[min_dist_f_index]
	vision_raycast.cast_to = (
		(food_node.global_position - global_position).normalized()
		* block_size
		* vision_distance
	)
	var ray_angle_to_facing = vision_raycast.cast_to.angle_to(orientation.cast_to)
	vision_raycast.force_raycast_update()
	var collider = vision_raycast.get_collider()
	if abs(ray_angle_to_facing) < PI / 3 && collider != null && collider.is_in_group("frogfood"):
		target_lost_timer.stop()
		target = collider
		food_target = collider
	elif target != null && target_lost_timer.is_stopped():
		target_lost_timer.start(loose_target_seconds)


func detect_player() -> void:
	var player
	if players.empty():
		# print("no player found")
		return

		#TODO Depends on height of blobby sprite since blobbys bottom and not his middle is on y=0
	player = players[0]
	#TODO Depends on height of blobby sprite since blobbys bottom and not his middle is on y=0
	vision_raycast.cast_to = (
		(player.global_position - global_position - Vector2(0, 9)).normalized()
		* block_size
		* vision_distance
	)
	var ray_angle_to_facing = vision_raycast.cast_to.angle_to(orientation.cast_to)
	vision_raycast.force_raycast_update()
	var collider = vision_raycast.get_collider()
	if abs(ray_angle_to_facing) < PI / 4 && collider != null && collider.is_in_group("player"):
		target_lost_timer.stop()
		target = collider
	elif target != null && target_lost_timer.is_stopped():
		target_lost_timer.start(loose_target_seconds)


func sleeping() -> Vector2:
	jump_timer.stop()
	# detect_player()
	return velocity


func loose_target() -> void:
	# print("frog target lost")
	target = null
	food_target = null


func jump():
	# print("jump calculation initiated")
	# Can only reverse once per jump calculation
	has_reversed = false
	var zero_vector = Vector2(0, 0)
	var v: Vector2 = velocity_for_jump_distance(default_jump_distance, deg2rad(default_jump_angle))
	v = correct_jump_direction(v)

	if is_bound:
		var next_position = global_position + v * current_delta
		var current_distance = global_position.distance_to(anchor.global_position)
		var new_distance = next_position.distance_to(anchor.global_position)
		# print(current_distance)
		# print(new_distance)
		# Would go out of distance
		if (
			(new_distance >= movement_radius && new_distance > current_distance)
			|| (new_distance > current_distance && was_restricted)
		):
			if state_machine.state == "hunting":
				barely_held_back_counter += 1
			if (
				can_reverse_facing_direction()
				&& (barely_held_back_counter == 0 || barely_held_back_counter > 1)
			):
				reverse_facing_direction()
		was_restricted = false

	if $Right_Wallcast.is_colliding() && $Left_Wallcast.is_colliding():
		# TODO No idea what it might do in these situations
		print("help this is a really tight space :(")
	elif get_facing_direction() < 0 && $Left_Wallcast.is_colliding():
		v = zero_vector
	elif get_facing_direction() > 0 && $Right_Wallcast.is_colliding():
		v = zero_vector

	v = correct_jump_direction(v)
	if v != zero_vector:
		v = consider_jump_headspace(v)
	if v != zero_vector:
		v = consider_jump_landing_space(v)
	if v == zero_vector:
		# TODO fix that you could call jump from jumping on top
		# and let it fail if the top is dangerous for jump height or not safe
		v = consider_jumping_on_top()
	if v == zero_vector && can_reverse_facing_direction():
		reverse_facing_direction()

#    if attached_player != null && v != zero_vector:
#        move_with_player(v)

	velocity = v


#func move_with_player(v: Vector2):
#    print(v)
#    attached_player.move_and_slide(v * 10)


func correct_jump_direction(v: Vector2) -> Vector2:
	if sign(v.x) != get_facing_direction():
		v.x *= -1
	return v


#	Cast a ray to the highest point of the jump
# Check the highest point for collision
# Calculate safe jump height and then a safe jump velocity
# Returns 0,0 if theres no headspace
func consider_jump_headspace(v: Vector2, recursive_check_count = 0, max_checks = 2) -> Vector2:
	if recursive_check_count >= max_checks:
		print("Frog has no safe headspace")
		return Vector2(0, 0)
	var height = calculate_jump_height(v)
	var distance = calculate_jump_distance(v)
	var angle = (v * get_facing_direction()).angle()
	# Half distance is an estimate of the jumps apex()
	#TODO Consider sprite size for height
	var height_collider = check_feeler(
		Vector2(get_facing_direction() * (distance / 2), -(height + 23))
	)
	if height_collider != null:
		var collision_point = feeler_raycast.get_collision_point()
		var target_height = collision_point.y - (feeler_raycast.global_position.y - 23)
		# print(feeler_raycast.global_position)
		var new_angle = angle * (0.75 if target_height > -26 else 0.95)
		var new_distance = abs(distance) * (0.66 if target_height < -26 else 0.75)
		v = velocity_for_jump_distance(new_distance, abs(new_angle))
		v = correct_jump_direction(v)
		height = calculate_jump_height(v) * -1
		distance = calculate_jump_distance(v) * get_facing_direction()
		if height < target_height && can_reverse_facing_direction():
			v = consider_jump_headspace(v, recursive_check_count + 1)
	return v


# Check the block in jump distance for danger or height
# If danger check neighboring blocks: if still danger, then jump closer (or jump over)
# If height move to distance which allows 1 block high jump
func consider_jump_landing_space(v: Vector2) -> Vector2:
	var jump_distance = calculate_jump_distance(v)
	var jump_height = calculate_jump_height(v)
	var collider = check_feeler(Vector2(jump_distance * get_facing_direction(), -jump_height / 2))
	# TODO Unpacked loop, make function or something?
	# Shortens the jump in steps to make it more safe
	if !is_jump_path_safe(v, global_position) || collider != null:
		jump_distance = calculate_jump_distance(v) - block_size / 1.5
		v = change_jump_distance(jump_distance, v)
		jump_height = calculate_jump_height(v)
		v = correct_jump_direction(v)
		collider = check_feeler(Vector2(jump_distance * get_facing_direction(), -jump_height / 2))
	if !is_jump_path_safe(v, global_position) || collider != null:
		jump_distance = calculate_jump_distance(v) - block_size / 2.0
		v = change_jump_distance(jump_distance, v)
		jump_height = calculate_jump_height(v)
		v = correct_jump_direction(v)
		collider = check_feeler(Vector2(jump_distance * get_facing_direction(), -jump_height / 2))
	if (
		(!is_jump_path_safe(v, global_position) || collider != null)
		&& can_reverse_facing_direction()
	):
		# Can be printed when frog would jump into a wall too
		print("at wall or no safe landing spot")
		return Vector2(0, 0)
	return v


func consider_jumping_on_top() -> Vector2:
	var collider = check_feeler(Vector2(42 * get_facing_direction(), 0))
	# 0 just for tile coordinate calculation
	var facing = 0 if get_facing_direction() >= 0 else -1
	if collider == null:
		return Vector2(0, 0)
	var local_position = tilemap.to_local(feeler_raycast.get_collision_point())
	var map_position = tilemap.world_to_map(local_position)
	var tile_position = Vector2(map_position.x + facing, map_position.y - 1)
	# TODO Here the climb height of frog is limited to one constantly
	var cell_id = tilemap.get_cell(tile_position.x, tile_position.y - 1)
	if (
		cell_id != -1
		#TODO 0 is the navigation tile, but thats subject to change!
		&& cell_id != 7
	):
		return Vector2(0, 0)
	var tile_upper_left_corner = tilemap.to_global(tilemap.map_to_world(tile_position))
	var tile_upper_right_corner = Vector2(
		tile_upper_left_corner.x + tilemap.cell_size.x, tile_upper_left_corner.y
	)

	var jump_angle = 0
	if facing < 0:
		var frog_bottom_left_corner = Vector2(
			$EnemyBody.global_position.x - $EnemyBody.shape.extents.x,
			$EnemyBody.global_position.y + $EnemyBody.shape.extents.y
		)
		jump_angle = frog_bottom_left_corner.angle_to_point(tile_upper_right_corner)
	else:
		var frog_bottom_right_corner = Vector2(
			$EnemyBody.global_position.x + $EnemyBody.shape.extents.x,
			$EnemyBody.global_position.y + $EnemyBody.shape.extents.y
		)
		jump_angle = frog_bottom_right_corner.angle_to_point(tile_upper_left_corner) - PI

	if abs(rad2deg(jump_angle)) < 78:
		return correct_jump_direction(
			velocity_for_jump_distance(default_jump_distance / 2, abs(deg2rad(80)))
		)
	else:
		var v = velocity_for_jump_distance(block_size / 1.5, abs(deg2rad(45)))
		return Vector2(v.x * -1 * get_facing_direction(), v.y)


# Tries to shorten the jump, so that it lands in a tiles center
func jump_to_tile_center(v: Vector2) -> Vector2:
	var distance = stepify(calculate_jump_distance(v), 0.01)
	if !is_equal_approx(
		fmod(abs(global_position.x + distance * get_facing_direction()), block_size),
		block_size / 2.0
	):
		# print(distance)
		# print(global_position.x + distance)
		# print(fmod((global_position.x + distance), block_size))
		var new_distance = distance
		if get_facing_direction() < 0:
			new_distance = (
				fmod(global_position.x + distance, block_size)
				- (block_size / 2.0)
				+ distance
			)
		else:
			new_distance = (
				distance
				+ block_size / 2.0
				- fmod(global_position.x + distance, block_size)
			)
		# print("centering distance")
		# print(new_distance)
		v = change_jump_distance(abs(new_distance), v)
	v = correct_jump_direction(v)
	return v


# TODO Depends on Frog Shape and Tile Shape
func is_jump_path_safe(v: Vector2, pos: Vector2) -> bool:
	var v0 = v.length()
	var angle = v.angle()
	var jump_distance = calculate_jump_distance(v)
	var harmful_nodes = get_tree().get_nodes_in_group("harmful")
	harmful_nodes.append_array(get_tree().get_nodes_in_group("pit"))
	var result = true
	for node in harmful_nodes:
		var node_pos = node.global_position
		# TODO Ignores spikes more than 4 blocks below and 3 jumps away
		# Also when its too near to one
		if (
			abs(node_pos.x - pos.x) > abs(jump_distance) * 3
			|| abs(node_pos.y - pos.y) > block_size * 4
			|| abs(node_pos.x - pos.x) < 1
		):
			continue
		var node_y = node_pos.y - block_size / 2.0
		var initial_throw_height = node_y - (global_position.y + 9)
		var term1 = (pow(v0, 2) * sin(2 * angle)) / (2 * _gravity)
		var term2 = (
			((v0 * cos(angle)) / _gravity)
			* sqrt(pow(v0, 2) * pow(sin(angle), 2) + 2 * _gravity * initial_throw_height)
		)
		var distance = abs(term1) + abs(term2)
		# print("distance to next spike")
		# print(pos.x + sign(v.x) * distance - node_pos.x)
		# TODO absolutly set sprite size
		var safe_distance = 24 / 2.0
		if sign(initial_throw_height) < 0:
			safe_distance = 24
		if abs(pos.x + sign(v.x) * distance - node_pos.x) < safe_distance:
			result = false
	return result


func calculate_jump_height(v: Vector2) -> float:
	return abs((pow(v.length(), 2) * pow(sin(v.angle()), 2)) / (2 * _gravity))


# Only works for jumps on straight ground
func calculate_jump_distance(v: Vector2) -> float:
	return abs((pow(v.length(), 2) * sin(-1 * 2 * v.angle())) / (_gravity))


func jump_height_to_velocity(target_height: float, v: Vector2) -> Vector2:
	var initial_height = calculate_jump_height(v)
	return v.normalized() * sqrt(pow(v.length(), 2) / (initial_height / target_height))


# Changes a Vector for a jump to the targeted distance, keeping the angle
func change_jump_distance(target_distance: float, v: Vector2) -> Vector2:
	var initial_distance = calculate_jump_distance(v)
	return v.normalized() * sqrt(pow(v.length(), 2) / (initial_distance / target_distance))


# Takes an angle and a distance to calculate a jump launching at that angle and covering the distance
func velocity_for_jump_distance(
	distance: float = default_jump_distance * block_size, angle: float = deg2rad(default_jump_angle)
) -> Vector2:
	var abs_velocity = sqrt((distance * _gravity) / sin(2 * angle))
	return Vector2(abs_velocity, 0).rotated(-1 * angle)


func can_reverse_facing_direction() -> bool:
	if is_on_floor() && !has_reversed:
		return true
	return false


# Returns a jump velocity that has the target_position in it's path
func attack_jump(target_position: Vector2) -> Vector2:
	var target_vector = target_position - global_position
	target_vector = Vector2(abs(target_vector.x), target_vector.y)
	var jump_angle = target_vector.angle()
	var v = Vector2()
	# TODO Tunable parameters
	if jump_angle < deg2rad(-30):
		v = velocity_for_jump_distance(target_vector.x, deg2rad(default_jump_angle))
		v = jump_height_to_velocity(abs(target_vector.y), v)
	else:
		v = velocity_for_jump_distance(target_vector.x * 1.5, deg2rad(45))
	v = correct_jump_direction(v)
	return v


# Checks the feeler ray for collisions and returns collider or null
func check_feeler(v: Vector2, _offset = Vector2(0, 0)) -> Object:
	var prev_position = feeler_raycast.position
	feeler_raycast.position += _offset
	feeler_raycast.cast_to = v
	feeler_raycast.force_raycast_update()
	var collider = feeler_raycast.get_collider()
	feeler_raycast.position = prev_position
	return collider


func reverse_facing_direction() -> void:
	has_reversed = true
	# print("reversing direction")
	orientation.cast_to.x *= -1


func get_facing_direction() -> float:
	return orientation.cast_to.x


func _on_HurtTimer_timeout() -> void:
	is_hurt = false
	#if(is_bound): add_to_group("harmful")
	$FrogSprite.material = null
