extends CharacterBody3D
class_name SimpleMovable

#needed to work as "tile"
@export var template_name = ""
var current_rotation = 0


#enums
#fixme: make DIRECTIONS unique
enum DIRECTIONS {UP, DOWN, LEFT, RIGHT, NONE}

#constants
const GRAVITY = 45


#exports
@export var move_speed = 8.0
#@export var can_slide = false
@export var can_push = true
@export var enable_gravity = true
@export var invincible = false
@export var vulnerable_to_spikes = true
@export var enable_rotation = false
@export var default_jump_velocity = 6.0



#vars
#general
var animated_mesh = null
var has_animated_mesh = false
var animation_player = null
#var level
var already_hit_by_spikes = false
var view_direction = DIRECTIONS.UP
var jump_direction
var driving_object = null
var driver = null
var is_blocked = false


#movement related
var enable_movement = true
var requested_direction = DIRECTIONS.NONE
var last_requested_direction = DIRECTIONS.NONE
var grid_size = 2
var enable_turning = true
var just_turned = false
var enable_left_turning = true
var enable_right_turning = true
var direction_change_cooldown = 0
var grid_distance_x
var grid_distance_z
var _move_dir_vector = Vector3(0, 0, 0)
var is_player_and_jumping = false
var saved_reset_position = Vector3(0, 0, 0)
var push_object = null
var is_pushing = false
var is_pushed = false
var current_move_speed = 0
var keys_amount_hack = 0
var start_frame = 0

#jumping
var is_jumping = false
var jump_triggered = false
var _jump_velocity = 0





#onready

@onready var rotation_point = $RotationPoint
@onready var mesh_instance = $RotationPoint/StaticMesh
@onready var ray_front_floor = $RotationPoint/FrontFloorRayCast
@onready var ray_ceiling = $RotationPoint/FrontCeilingRayCast
@onready var ray_left_floor = $RotationPoint/LeftFloorRayCast
@onready var ray_right_floor = $RotationPoint/RightFloorRayCast
@onready var ray_left_wall = $RotationPoint/LeftWallRayCast
@onready var ray_right_wall = $RotationPoint/RightWallRayCast
@onready var ray_front = $RotationPoint/FrontRayCast
@onready var ray_floor = $RotationPoint/FloorRayCast
@onready var front_area = $RotationPoint/FrontArea
@onready var collision_shape = $CollisionShape3D

@onready var health_node = $Health






#----------- built-in functions -----------
func _ready() -> void:
	direction_change_cooldown = 0
	_jump_velocity = default_jump_velocity
	grid_distance_x = abs(fmod(global_position.x - 1, grid_size))
	grid_distance_z = abs(fmod(global_position.z - 1, grid_size))
	
	
	
	saved_reset_position = global_position
	
	

	animated_mesh = find_child("AnimatedMesh")
	if animated_mesh != null:
		has_animated_mesh = true
		animated_mesh.visible = true
		mesh_instance.visible = false
		animation_player = animated_mesh.find_child("AnimationPlayer")
		
	requested_direction = DIRECTIONS.NONE
	
	#update_detection_area()
	#level = get_tree().get_nodes_in_group("level")[0]
	already_hit_by_spikes = false

func _physics_process(delta):
	start_frame += 1
	if LevelManager.exec_context != "game":
		return
	
	
	grid_distance_x = 1 - abs(abs(roundf(global_position.x * 0.5)) - abs((global_position.x * 0.5))) * 2
	grid_distance_z = 1 - abs(abs(roundf(global_position.z * 0.5)) - abs((global_position.z * 0.5))) * 2
	#print ("grid dist x: " + str(grid_distance_x) + ", z: " + str(grid_distance_z))
	direction_change_cooldown += 1
	
	if grid_distance_x < 0.1 and grid_distance_z < 0.1 and is_on_floor():
		saved_reset_position = global_position
	
	#print ("grid dist x: " + str(grid_distance_x) + " / grid dist z: " + str(grid_distance_z))
	
	if enable_gravity:
		velocity.y -= GRAVITY * delta
	
	
		
		
	_calculate_movement(delta)
	_apply_movement(delta)
	if push_object != null and push_object.is_in_group("pushable"):
		if is_pushing:
			push_object.current_move_speed = current_move_speed
			if requested_direction != DIRECTIONS.NONE and front_area.overlaps_body(push_object):
		
				push(push_object, requested_direction)
			elif push_object.get_parent() != null and push_object.get_parent().is_in_group("pushable"):
				push(push_object, requested_direction)

		


	
			
			
	
	jump_triggered = false
	if is_on_floor():
		is_jumping = false
	last_requested_direction = requested_direction
	
	

# calculates the actual movement based on the movement request
func _calculate_movement(delta):
	
	if driving_object != null:
		return
	
	if !is_on_floor():
		if not is_jumping:
			requested_direction = DIRECTIONS.NONE
		return
	
	_move_dir_vector = direction_to_vector(view_direction)
	
	if requested_direction != DIRECTIONS.NONE and keys_amount_hack >= 2:
		enable_left_turning = ray_left_floor.is_colliding()
		var leftObject = ray_left_floor.get_collider()
		if leftObject != null:
			if leftObject.is_in_group("ignore_raycast"):
				enable_left_turning = false
			elif ray_left_floor.get_collision_point().y - global_position.y < -0.7 and (grid_distance_x < 0.44 and grid_distance_z < 0.44):
				enable_left_turning = false
		enable_right_turning = ray_right_floor.is_colliding()
		var rightObject = ray_right_floor.get_collider()
		if rightObject != null:
			if rightObject.is_in_group("ignore_raycast"):
				enable_right_turning = false
			elif ray_right_floor.get_collision_point().y - global_position.y < -0.7 and (grid_distance_x < 0.44 and grid_distance_z < 0.44):
				enable_right_turning = false
		#hack: check for what I needed the hack, if it can be made without it
		if last_requested_direction != DIRECTIONS.NONE and keys_amount_hack >= 2:

			if ray_left_wall.is_colliding():
				enable_left_turning = false
			if ray_right_wall.is_colliding():
				enable_right_turning = false
	else:
		enable_left_turning = true
		enable_right_turning = true

	

	if requested_direction != DIRECTIONS.NONE:

		#only allow turning when closer to the given distance from tile center
		#this value should not be changed by default (works good with = 0.3)
		if (grid_distance_x < 0.3 and grid_distance_z < 0.3): 
			#rc_holder.update_raycast_direction(requested_direction)
			update_rotation_point(requested_direction)
			_move_dir_vector = direction_to_vector(view_direction)
			
		# add an exception to allow 180° direction change
		if requested_direction == get_opposite_direction(view_direction): 
			#rc_holder.update_raycast_direction(requested_direction)
			update_rotation_point(requested_direction)
			_move_dir_vector = direction_to_vector(view_direction)
		
		#move player to x / z center when not centered while walking

		if view_direction == DIRECTIONS.LEFT or view_direction == DIRECTIONS.RIGHT:
			if grid_distance_x < 1.9:
				_move_dir_vector.z = -(fposmod(global_position.z, grid_size) - 1) * move_speed * grid_distance_x
		if view_direction == DIRECTIONS.UP or view_direction == DIRECTIONS.DOWN:
			if grid_distance_z < 1.9:
				_move_dir_vector.x = -(fposmod(global_position.x, grid_size) - 1) * move_speed * grid_distance_z
						

func _jump_movement(delta):
	if jump_triggered:
		
		if requested_direction != DIRECTIONS.NONE and requested_direction != view_direction:
			requested_direction = view_direction
			update_rotation_point(requested_direction)
		
		_move_dir_vector = direction_to_vector(requested_direction)
		velocity.x = _move_dir_vector.x * move_speed * 0.6
		velocity.z = _move_dir_vector.z * move_speed * 0.6
		velocity.y = _jump_velocity
	elif is_jumping:
		#print ("jvel while jumping: x: " + str(velocity.x) + ", z: " + str(velocity.z))
		if requested_direction != DIRECTIONS.NONE:
			if abs(velocity.x) < 1.3:
				velocity.x = _move_dir_vector.x * move_speed * 0.5
			if abs(velocity.z) < 1.3:
				velocity.z = _move_dir_vector.z * move_speed * 0.5
		var found_floor = ray_floor.is_colliding()
		var floorObject = ray_floor.get_collider()
		#fixme only do it when it is the player
		if floorObject != null and velocity.y < -0.5:
			if floorObject.is_in_group("drivable"):
				velocity.y -= GRAVITY * delta * 3
				velocity.x = move_toward(velocity.z, 0, move_speed)
				velocity.z = move_toward(velocity.x, 0, move_speed)
				
		#
	if view_direction == DIRECTIONS.LEFT or view_direction == DIRECTIONS.RIGHT:
		velocity.z = move_toward(velocity.x, 0, move_speed)
	elif view_direction == DIRECTIONS.UP or view_direction == DIRECTIONS.DOWN:
		velocity.x = move_toward(velocity.z, 0, move_speed)
		


func _apply_movement(delta):
	var speed_mod = 1.0
	if is_pushing:
		speed_mod = 0.5
	if !is_pushed:
		current_move_speed = move_speed * speed_mod
			


	if _move_dir_vector and can_move() == true:
		if not is_jumping:
			if is_on_floor() and requested_direction != DIRECTIONS.NONE:
				velocity.x = _move_dir_vector.x * current_move_speed
				velocity.z = _move_dir_vector.z * current_move_speed
				
						
			else:	
				velocity.x = move_toward(velocity.x, 0, current_move_speed)
				velocity.z = move_toward(velocity.z, 0, current_move_speed)
		else:
			_jump_movement(delta)	
	else:
		if is_jumping:
			_jump_movement(delta)
		velocity.x = move_toward(velocity.x, 0, current_move_speed)
		velocity.z = move_toward(velocity.z, 0, current_move_speed)
		
	if driving_object == null or is_jumping:
		 #only apply movement after the first time frame is over, prevents an ugly bug of objeckts snapping if they are 0
		if start_frame > 1: 
		
			move_and_slide()
	else:
		#if driving_object.has_node()
		var anchor = driving_object.get_node_or_null("DriveArea/DriveAnchor")
		var anchor_pos = driving_object.global_position + anchor.position
		if anchor != null:
			global_position.x = anchor_pos.x
			global_position.y = anchor_pos.y
			global_position.z = anchor_pos.z
	#move_and_slide()
	
	
func jump(_jump_vel = default_jump_velocity):
	if LevelManager.exec_context != "game":
		return
	
	
	if is_on_floor() and (grid_distance_x < 0.6 or grid_distance_z < 0.6):
		if requested_direction == DIRECTIONS.LEFT or requested_direction == DIRECTIONS.RIGHT:
			if grid_distance_z > 0.4:
				return
		if requested_direction == DIRECTIONS.UP or requested_direction == DIRECTIONS.DOWN:
			if grid_distance_x > 0.4:
				return
		update_rotation_point(requested_direction)
		#print("jumping: " + str(direction_to_string(requested_direction)))
		jump_triggered = true
		is_jumping = true
		
		_jump_velocity = _jump_vel

		

func can_move(false_if_close_distance = true) -> bool:
	if LevelManager.exec_context != "game":
		return false
	if enable_movement == false or is_blocked == true:
		return false
		
	var found_floor_front = ray_front_floor.is_colliding()
	var floorObject = ray_front_floor.get_collider()

	
		
	if floorObject != null:
		if floorObject.is_in_group("ignore_raycast"):
			#print ("oh - abyss detected")
			found_floor_front = false
		elif is_on_floor() and jump_triggered == false and false_if_close_distance: 
			#todo: check if there is something to fix, or why I marked this as todo
			if ray_front_floor.get_collision_point().y - global_position.y < -0.7 and (grid_distance_x < 0.44 and grid_distance_z < 0.44):
				found_floor_front = false
			else:
				found_floor_front = true
			
	return found_floor_front == true
	
func has_front_collision_detected() -> bool:
	for body in front_area.get_overlapping_bodies():
		if body != self:
			return true
	return false


func reset_position():
	print ("reset position called !!!")
	global_position = saved_reset_position


#----------- physics related  -----------
func on_floor_or_flying() -> bool:
	return is_on_floor() or enable_gravity == false

func is_on_slope() -> bool:
	return get_floor_normal().y <= 0.96 and on_floor_or_flying()

func hit():
	pass

func push(object_to_push: SimpleMovable, direction: DIRECTIONS):
	if LevelManager.exec_context != "game":
		return false
	
	if object_to_push.enable_movement == false or !is_on_floor() or object_to_push.is_blocked == true:
		return false

	object_to_push.requested_direction = direction
	
	var x_offset = global_position.x + (direction_to_vector(requested_direction).x * 2)
	var z_offset = global_position.z + (direction_to_vector(requested_direction).z * 2)
	
	if abs(x_offset  - object_to_push.global_position.x) > 0.01 or abs(z_offset  - object_to_push.global_position.z) > 0.01:
		return false


func get_opposite_direction(direction):
	if direction == DIRECTIONS.LEFT:
		return DIRECTIONS.RIGHT
	elif direction == DIRECTIONS.RIGHT:
		return DIRECTIONS.LEFT
	elif direction == DIRECTIONS.UP:
		return DIRECTIONS.DOWN
	elif direction == DIRECTIONS.DOWN:
		return DIRECTIONS.UP	
	else:
		return DIRECTIONS.NONE
		
func direction_to_vector(direction) -> Vector3:
	var dir = Vector3(0, 0, 0)
	
	if direction == DIRECTIONS.UP:	
		dir = Vector3(0, 0, -1)
	elif direction == DIRECTIONS.DOWN:	
		dir = Vector3(0, 0, 1)
	elif direction == DIRECTIONS.LEFT:	
		dir = Vector3(-1, 0, 0)
	elif direction == DIRECTIONS.RIGHT:
		dir = Vector3(1, 0, 0)
	return dir

func update_rotation_point(direction):
	
	if direction_change_cooldown < 5 or enable_turning == false:
		return
	elif enable_left_turning == false and is_left_turn_request():
		return
	elif enable_right_turning == false and is_right_turn_request():
		return
	if direction != DIRECTIONS.NONE:
		view_direction = direction
		direction_change_cooldown = 0
	match direction:
		DIRECTIONS.UP:
			rotation_point.rotation = Vector3(0,0,0)
		DIRECTIONS.DOWN:
			rotation_point.rotation = Vector3(0,deg_to_rad(180),0)
		DIRECTIONS.LEFT:
			rotation_point.rotation = Vector3(0,deg_to_rad(90),0)
		DIRECTIONS.RIGHT:
			rotation_point.rotation = Vector3(0,deg_to_rad(-90),0)
	
	
func is_left_turn_request() -> bool:
	if view_direction == DIRECTIONS.UP:
		return requested_direction == DIRECTIONS.LEFT
	elif view_direction == DIRECTIONS.DOWN:
		return requested_direction == DIRECTIONS.RIGHT
	elif view_direction == DIRECTIONS.RIGHT:
		return requested_direction == DIRECTIONS.UP
	elif view_direction == DIRECTIONS.LEFT:
		return requested_direction == DIRECTIONS.DOWN
	else:
		
		return false
		
func is_right_turn_request() -> bool:
	if view_direction == DIRECTIONS.UP:
		return requested_direction == DIRECTIONS.RIGHT
	elif view_direction == DIRECTIONS.DOWN:
		return requested_direction == DIRECTIONS.LEFT
	elif view_direction == DIRECTIONS.RIGHT:
		return requested_direction == DIRECTIONS.DOWN
	elif view_direction == DIRECTIONS.LEFT:
		return requested_direction == DIRECTIONS.UP
	else:
		return false

func direction_to_string(direction):
	if direction == DIRECTIONS.UP:
		return "UP"
	elif direction == DIRECTIONS.DOWN:
		return "DOWN"
	elif direction == DIRECTIONS.RIGHT:
		return "RIGHT"
	elif direction == DIRECTIONS.LEFT:
		return "LEFT"
	else:	
		return "NONE(or up)"
		


func _on_front_area_body_entered(body):
	if !can_push:
		return
	if body != null and body.is_in_group("pushable") and body != self:
		push_object = body
		is_pushing = true
		body.is_pushed = true
		


func _on_front_area_body_exited(body):
	if body != null and body.is_in_group("pushable") and body != self:
		is_pushing = false
		body.is_pushed = false
		body.requested_direction = DIRECTIONS.NONE
		requested_direction = DIRECTIONS.NONE


#needed to work as "tile"		
func get_dict():
	var element_rotation = self.get_rotation_degrees()

	return {
		"name" : self.template_name,
		"rotation" : 0,
	}
