# Hurry Curry! - a game about cooking
# Copyright (C) 2025 Hurry Curry! contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
class_name ControllablePlayer
extends Player

const PLAYER_SPEED = 55
const PLAYER_FRICTION = 15
const BOOST_FACTOR = 2.5
const BOOST_DURATION = 0.3
const BOOST_RESTORE = 0.5
const MAX_PLAYER_INTERACT_DIST := 1.9

var onscreen_controls = preload("res://player/onscreen_controls/controls.tscn").instantiate()
var marker: Marker = preload("res://player/marker/marker.tscn").instantiate()

var facing := Vector2(1, 0)
var velocity_ := Vector2(0, 0)
var direction := Vector2(0, 0)
var stamina := 0.
var chat_open := false
var input_rotation = 0

var vibration_timer := Timer.new()
var current_vibration_strength := 0.
var current_vibration_change := 0.

var target_tile: Vector2i = Vector2i.ZERO
var target_visual: Vector3 = Vector3.ZERO
var found_interact_target := false

var last_interaction = null # : Vector2i?

func _ready():
	vibration_timer = Timer.new()
	vibration_timer.wait_time = 0.1
	vibration_timer.timeout.connect(_on_vibration_timeout)
	add_child(vibration_timer)
	var timer = Timer.new()
	timer.one_shot = false
	timer.wait_time = 1. / 50.
	add_child(timer)
	timer.start()
	timer.connect("timeout", func():
		if game.mp != null and game.join_state == Game.JoinState.JOINED:
			game.mp.send_movement(game.my_player_id, position_, direction, boosting)
	)
	add_child(onscreen_controls)
	add_child(marker)
	super()
	Settings.hook_changed_init("gameplay.first_person", self, func(fps):
		character.head_default.visible = not fps
		character.username_tag.visible = not fps
	)

const MAX_DT = 1. / 50.
func _process(delta):
	super(delta)
	marker.position = G.interpolate(marker.position, target_visual, delta * 30.)
	while delta > 0.001:
		var dt = min(delta, MAX_DT)
		_process_movement(dt)
		delta -= dt
	update_touch_scrolls()

func _input(event: InputEvent):
	if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED: return
	if event is InputEventMouseMotion:
		var sens = Settings.read("input.fps_mouse_sensitivity")
		fps_look.x += event.relative.y * -sens
		fps_look.y += event.relative.x * sens
		fps_look.x = max(-PI * 0.45, min(PI * 0.45, fps_look.x))
	if event is InputEventMouseButton:
		var action = ""
		match event.button_index:
			MOUSE_BUTTON_LEFT: action = "interact_left"
			MOUSE_BUTTON_RIGHT: action = "interact_right"
			_: return
		if event.pressed: Input.action_press(action)
		else: Input.action_release(action)
	

var moving_duration = 0
var fps_look = Vector2(0., 0.)
func _process_movement(delta):
	var input = Input.get_vector("left", "right", "forwards", "backwards") if is_input_enabled() else Vector2.ZERO
	if Settings.read("gameplay.first_person"): input = (input + Vector2(0.0, -0.1)).rotated(fps_look.y)
	else: input = input.rotated(input_rotation)

	var boost = Input.is_action_pressed("boost") or (Settings.read("gameplay.latch_boost") and boosting)

	if (Input.is_action_pressed("interact_left")
		or Input.is_action_just_released("interact_left")
		or Input.is_action_pressed("interact_right")
		or Input.is_action_just_released("interact_right")):
		input *= 0
	else:
		update_interact_target()

	if Settings.read("gameplay.accessible_movement"): 
		if input.length() < 0.5: moving_duration -= delta * 2
		else: moving_duration += delta
		moving_duration = clamp(moving_duration, 0, 1)
		input *= min(1, moving_duration)

	interact()
	var was_boosting = boosting
	direction = input
	update(delta, boost)
	if boosting and not was_boosting and Settings.read("gameplay.vibration"):
		Input.start_joy_vibration(0, 0, input.length(), 0.15)
		Input.vibrate_handheld(75, input.length() * 0.1)
	walking = input.length_squared() > 0.1
	position_anim = position_
	rotation_anim = rotation_

func update(dt: float, boost: bool):
	direction = direction.limit_length(1.)
	if direction.length() > 0.05:
		facing = direction + (facing - direction) * exp(-dt * 10.)
	if direction.length() < 0.5:
		direction *= 0
	rotation_ = atan2(facing.x, facing.y);
	boost = boost and direction.length() > 0.1
	boosting = boost and (boosting or stamina >= 1.0) and stamina > 0
	if boosting: stamina -= dt / BOOST_DURATION
	else: stamina += dt / BOOST_RESTORE
	stamina = max(min(stamina, 1.0), 0.0)
	var speed = PLAYER_SPEED * (BOOST_FACTOR if boosting else 1.)
	velocity_ += direction * dt * speed
	position_ += velocity_ * dt
	velocity_ = velocity_ * exp(-dt * PLAYER_FRICTION)
	collide(dt)

func collide(dt: float):
	for xo in range(-1, 2):
		for yo in range(-1, 2):
			var tile = Vector2i(xo, yo) + Vector2i(position_)
			if !game.get_tile_collision(tile): continue
			tile = Vector2(tile)
			var d = aabb_point_distance(tile, tile + Vector2.ONE, position_)
			if d > PLAYER_SIZE: continue
			var h = 0.01;
			var d_sample_x = aabb_point_distance(tile, tile + Vector2.ONE, position_ + Vector2(h, 0))
			var d_sample_y = aabb_point_distance(tile, tile + Vector2.ONE, position_ + Vector2(0, h))
			var grad = (Vector2(d_sample_x - d, d_sample_y - d)) / h

			position_ += (PLAYER_SIZE - d) * grad;
			velocity_ -= grad * grad.dot(velocity_)

	for player: Player in game.players.values():
		var diff = position_ - player.position_
		var d = diff.length()
		if d < 0.01: continue
		if d >= PLAYER_SIZE * 2: continue
		var norm = diff.normalized();
		var f = 100 / (1 + d)
		velocity_.x += norm.x * f * dt
		velocity_.y += norm.y * f * dt

func is_input_enabled() -> bool:
	return not game.menu.covered and not Global.game_paused

func update_touch_scrolls():
	# TODO: Don't call this function every frame, but only when input menu
	# covered value is updated
	if onscreen_controls.touch_enabled:
		onscreen_controls.visible = is_input_enabled()

func aabb_point_distance(mi: Vector2, ma: Vector2, p: Vector2) -> float:
	return (p - p.clamp(mi, ma)).length()

func update_position(_new_position: Vector2, _new_rotation: float, _new_boosting: bool):
	pass

func progress(position__: float, speed: float, warn: bool, h):
	super(position__, speed, warn, h)
	if warn:
		current_vibration_strength = position__
		current_vibration_change = speed
		var vibration_strength := pow(current_vibration_strength, 3)
		if Settings.read("gameplay.vibration"): # todo maybe include the lines above too
			Input.start_joy_vibration(0, vibration_strength, 0, 0.1)
			Input.vibrate_handheld(100, vibration_strength)
		vibration_timer.start()
	if speed == 0:
		current_vibration_strength = 0.
		vibration_timer.stop()

func _on_vibration_timeout():
	current_vibration_strength = clampf(current_vibration_strength + current_vibration_change * 0.1, 0, 1)
	if current_vibration_strength == 0.:
		return
	var vibration_strength := pow(current_vibration_strength, 3)
	Input.start_joy_vibration(0, vibration_strength, 0, 0.1)
	Input.vibrate_handheld(100, vibration_strength)
	vibration_timer.start()

func put_item(tile: Tile, h: int):
	super(tile, h)
	if Settings.read("gameplay.vibration"):
		Input.start_joy_vibration(0, 0.1, 0.0, 0.075)
		Input.vibrate_handheld(75, 0.1)

func take_item(tile: Tile, h: int):
	super(tile, h)
	if Settings.read("gameplay.vibration"):
		Input.start_joy_vibration(0, 0.1, 0.0, 0.075)
		Input.vibrate_handheld(75, 0.1)

func interact():
	if not is_input_enabled(): return
	
	var tile = game.map.get_tile_instance(target_tile)
	if tile != null:
		if not marker.visible:
			marker.visible = true
			marker.position = target_visual

		# clear last interaction if target_tile has moved since
		if last_interaction != null and not last_interaction == target_tile:
			game.mp.send_tile_interact(game.my_player_id, last_interaction, false, 0)
			marker.set_interacting(false)
			last_interaction = null
		marker.set_interactive(found_interact_target)
		if found_interact_target:
			for h in [0, 1]:
				if Input.is_action_just_pressed("interact_" + G.index_to_hand(h)) and last_interaction == null:
					last_interaction = target_tile
					game.mp.send_tile_interact(game.my_player_id, target_tile, true, h if h < Global.hand_count else 0)
					tile.interact()
					marker.set_interacting(true)
				if Input.is_action_just_released("interact_" + G.index_to_hand(h)):
					last_interaction = null
					game.mp.send_tile_interact(game.my_player_id, target_tile, false, h if h < Global.hand_count else 0)
					marker.set_interacting(false)
	else:
		marker.visible = false

func update_interact_target():
	if Settings.read("gameplay.first_person"):
		return update_interact_target_fps()
	match Settings.read("gameplay.interact_target"):
		"dirsnap": return update_interact_target_dirsnap()
		"dir": return update_interact_target_dir()
		_: return update_interact_target_dir()

func update_interact_target_dir():
	target_tile = Vector2i(
		int(floor(movement_base.position.x + sin(movement_base.rotation.y))),
		int(floor(movement_base.position.z + cos(movement_base.rotation.y)))
	)
	var tile = game.map.get_tile_instance(target_tile)
	if tile != null:
		found_interact_target = game.get_tile_interactive(target_tile, hand)
		target_visual = tile.item_base.global_position
	else:
		found_interact_target = false
		target_visual = Vector3(float(target_tile.x), 1., float(target_tile.y))

func update_interact_target_fps():
	target_tile = Vector2i(
		int(floor(movement_base.position.x + sin(-fps_look.y + PI))),
		int(floor(movement_base.position.z + cos(-fps_look.y + PI)))
	)
	var tile = game.map.get_tile_instance(target_tile)
	if tile != null:
		found_interact_target = game.get_tile_interactive(target_tile, hand)
		target_visual = tile.item_base.global_position
	else:
		found_interact_target = false
		target_visual = Vector3(float(target_tile.x), 1., float(target_tile.y))


func update_interact_target_dirsnap():
	var interact_target := Vector2(
		movement_base.position.x + sin(movement_base.rotation.y) * 0.7,
		movement_base.position.z + cos(movement_base.rotation.y) * 0.7
	)
	var interact_target_i := interact_target.floor()
	target_visual = Vector3(interact_target_i.x, 0, interact_target_i.y)
	target_tile = interact_target_i
	var best_distance := 100.
	found_interact_target = false

	# Calculate player positions. Tiles with players on them are valid interact targets.
	var player_positions: Dictionary = {} # Dictionary[id: int, pos: Vector3]
	for p: Player in game.players.values():
		if not p.is_chef: continue
		player_positions[p.id] = Vector3(p.position_anim.x, 0, p.position_anim.y)

	# Test all tiles in a 3x3 square around the player for interactible tiles.
	# Return the one which is closest to interact_target.
	for offset_x in range(-1, 2):
		for offset_y in range (-1, 2):
			var offset_cursor := interact_target_i + Vector2(offset_x, offset_y)
			var tile_center := Vector2(offset_cursor) + Vector2(0.5, 0.5)
			
			if game.get_tile_interactive(offset_cursor, hand):
				var cursor_tile_distance := interact_target.distance_to(tile_center)
				var player_tile_distance := Vector2(
					movement_base.position.x - tile_center.x,
					movement_base.position.z - tile_center.y
					).length()
				if player_tile_distance < MAX_PLAYER_INTERACT_DIST && cursor_tile_distance < best_distance:
					found_interact_target = true
					best_distance = cursor_tile_distance
					var tile = game.map.get_tile_instance(offset_cursor)
					target_visual = tile.item_base.global_position
					if tile.item != null and not tile.item.is_round():
						marker.rotation.y = tile.item.rotation.y
					else: marker.rotation.y = 0.
					target_tile = offset_cursor
				continue
			
			# Check if there are any players on this tile.
			# If there are multilpe, remember the player closest to the center of the tile
			# (They will be the interact target)
			var best_cursor_tile_distance := 100.
			var best_player_tile_distance := 100.
			var best_player_pos: Vector3
			var best_player_rot: float
			for p_id in player_positions.keys():
				if p_id == game.my_player_id: continue # I can't interact with myself
				var p_pos: Vector3 = player_positions[p_id]
				var p_pos_2d := Vector2(p_pos.x, p_pos.z)
				var tile_center_distance := p_pos_2d.distance_to(tile_center)
				var cursor_tile_distance := interact_target.distance_to(tile_center)
				var player_tile_distance := Vector2(movement_base.position.x, movement_base.position.z).distance_to(tile_center)
				if tile_center_distance < 0.7:
					if cursor_tile_distance < best_cursor_tile_distance:
						best_cursor_tile_distance = cursor_tile_distance
						best_player_tile_distance = player_tile_distance
						best_player_pos = p_pos
						best_player_rot = game.players[p_id].rotation_anim
			
			if best_player_tile_distance < MAX_PLAYER_INTERACT_DIST && best_cursor_tile_distance < best_distance:
				found_interact_target = true
				best_distance = best_cursor_tile_distance
				target_visual = best_player_pos
				marker.rotation.y = best_player_rot
				target_tile = offset_cursor
