# Hurry Curry! - a game about cooking
# Copyright (C) 2026 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

@abstract class InteractTarget:
	var target_visual_pos: Vector3
	var target_visual_rot: float

class EmptyTarget extends InteractTarget:
	pass

class PlayerTarget extends InteractTarget:
	var player: Player
	var distance: float
	func _init(player_: Player, target_visual_pos_: Vector3, target_visual_rot_: float, distance_: float) -> void:
		player = player_
		target_visual_pos = target_visual_pos_
		target_visual_rot = target_visual_rot_
		distance = distance_

class TileTarget extends InteractTarget:
	var pos: Vector2i
	var tile: Tile
	func _init(pos_: Vector2i, tile_: Tile, target_visual_pos_: Vector3, target_visual_rot_: float) -> void:
		pos = pos_
		tile = tile_
		target_visual_pos = target_visual_pos_
		target_visual_rot = target_visual_rot_

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 current_interact_target: InteractTarget = EmptyTarget.new()
var chat_open := false
var input_rotation = 0

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

var was_boosting = boosting

func _ready():
	vibration_timer = Timer.new()
	vibration_timer.wait_time = 0.1
	vibration_timer.timeout.connect(_on_vibration_timeout)
	add_child(vibration_timer)
	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
	)

func _process(delta):
	super(delta)
	marker.position = G.interpolate(marker.position, current_interact_target.target_visual_pos, delta * 30.)
	if is_despawning: return
	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 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 Input.is_action_pressed("stop_movement"): input *= 0.4
	if Settings.read("gameplay.first_person"): input = (input + Vector2(0.0, -0.1)).rotated(fps_look.y)
	else: input = input.rotated(input_rotation)


	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:
		if Settings.read("gameplay.first_person"): current_interact_target = get_interact_target_fps()  
		else: current_interact_target = get_interact_target()

	interact(current_interact_target)

	var boost = Input.is_action_pressed("boost") or (Settings.read("gameplay.latch_boost") and boosting)
	
	if direction != input or boosting != was_boosting:
		if game.mp != null and game.join_state == Game.JoinState.JOINED:
			# print("sending %s %s" % [boost, was_boosting])
			game.mp.send_movement(game.my_player_id, position_, input, boost)
	
	was_boosting = boosting
	direction = input
	update_movement(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 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 update_position(_new_position: Vector2, _new_direction: Vector2, _new_rotation: float, _new_boosting: bool):
	# Ignore movement packets from server. (This is not a movement sync)
	pass

func progress(position__: float, speed: float, warn: bool, h):
	# An item the player is holding is making progress
	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(interact_target: InteractTarget):
	if not is_input_enabled(): return
	
	if interact_target is EmptyTarget:
		marker.visible = false
		marker.rotation.y = 0.
		return
	else:
		if not marker.visible:
			# New target acquired!
			marker.visible = true
			marker.position = interact_target.target_visual_pos
			marker.rotation.y = interact_target.target_visual_rot
	
	if interact_target is TileTarget:
		for h in [0, 1]:
			var hand_: int = h if h < Global.hand_count else 0
			if Input.is_action_just_pressed("interact_" + G.index_to_hand(h)) and not marker.interacting:
				game.mp.send_tile_interact(game.my_player_id, interact_target.pos, true, hand_)
				interact_target.tile.interact()
				marker.set_interacting(true)
			if Input.is_action_just_released("interact_" + G.index_to_hand(h)):
				game.mp.send_tile_interact(game.my_player_id, interact_target.pos, false, hand_)
				marker.set_interacting(false)
	elif interact_target is PlayerTarget:
		for h in [0, 1]:
			var hand_: int = h if h < Global.hand_count else 0
			if Input.is_action_just_pressed("interact_" + G.index_to_hand(h)) and not marker.interacting:
				game.mp.send_player_interact(game.my_player_id, interact_target.player.id, hand_, true, hand_)
				marker.set_interacting(true)
			if Input.is_action_just_released("interact_" + G.index_to_hand(h)):
				game.mp.send_player_interact(game.my_player_id, interact_target.player.id, hand_, false, hand_)
				marker.set_interacting(false)

func get_interact_target_fps() -> InteractTarget:
	var best_interact_target: InteractTarget = EmptyTarget.new()
	var 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 target = get_best_player_target(target_tile, Vector2(movement_base.position.x, movement_base.position.z))
	if target is PlayerTarget and target.distance < 1.25:
		best_interact_target = target

	var tile = game.map.get_topmost_instance(target_tile)
	if game.is_tile_interactive(target_tile, hand):
		var target_rot: float = tile.item.rotation.y if tile.item != null and not tile.item.is_round() else 0.
		best_interact_target = TileTarget.new(target_tile, tile, tile.item_base.global_position, target_rot)
	
	return best_interact_target

func get_interact_target() -> InteractTarget:
	var movement_base_2d := Vector2(movement_base.position.x, movement_base.position.z)
	var interact_target_pos := movement_base_2d + Vector2(
		sin(movement_base.rotation.y) * 0.7,
		cos(movement_base.rotation.y) * 0.7
	)
	var interact_target_pos_i := interact_target_pos.floor()

	# Find the player closest to the interact target
	var best_interact_target: InteractTarget = EmptyTarget.new()
	if Settings.read("gameplay.allow_focus_players"):
		best_interact_target = get_best_player_target(interact_target_pos, movement_base_2d)
	var best_distance: float = best_interact_target.distance if best_interact_target is PlayerTarget else 100.

	# Let's see if we can find a tile wsith an even better distance!
	# Test all tiles in a 3x3 square around the player for interactible tiles.
	# Return the one which is closest to interact_target_pos.
	for offset_x in range(-1, 2):
		for offset_y in range(-1, 2):
			var offset_cursor := interact_target_pos_i + Vector2(offset_x, offset_y)
			var tile_center := Vector2(offset_cursor) + Vector2(0.5, 0.5)
			
			if game.is_tile_interactive(offset_cursor, hand):
				var distance := interact_target_pos.distance_to(tile_center)
				var self_target_distance := movement_base_2d.distance_to(tile_center)
				if self_target_distance < MAX_PLAYER_INTERACT_DIST && distance < best_distance:
					best_distance = distance
					var tile = game.map.get_topmost_instance(offset_cursor)
					var tile_item = game.map.get_tile_item(offset_cursor)
					var target_rot: float = tile_item.rotation.y if tile_item != null and not tile_item.is_round() else 0.
					best_interact_target = TileTarget.new(offset_cursor, tile, tile.item_base.global_position, target_rot)
	
	return best_interact_target

func get_best_player_target(interact_target_pos: Vector2, movement_base_2d: Vector2) -> InteractTarget:
	var best_interact_target: InteractTarget = EmptyTarget.new()
	var best_distance := 100.

	for p: Player in game.players.values():
		if not p.is_chef: continue # Customers cannot be interacted with
		if p.id == game.my_player_id: continue # I can't interact with myself
		
		var interact_possible := false
		for i in range(p.hand.size()):
			interact_possible = interact_possible or (p.hand[i] != null or p.hand[i] != null)
		if not interact_possible: continue # Interaction not possible if all hand pairs are empty
		
		var distance: float = p.position_anim.distance_to(interact_target_pos)
		var self_target_distance: float = p.position_anim.distance_to(movement_base_2d)
		if self_target_distance < MAX_PLAYER_INTERACT_DIST and distance < best_distance:
			best_distance = distance
			best_interact_target = PlayerTarget.new(p, Vector3(p.position_anim.x, 0, p.position_anim.y), p.rotation_anim, distance)
	
	return best_interact_target
