# 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 Player
extends Node3D

const CHAT_BUBBLE: PackedScene = preload("res://player/chat_bubble.tscn")
const ITEM_BUBBLE: PackedScene = preload("res://player/item_bubble.tscn")

const PLAYER_SIZE: float = 0.4
const PLAYER_ACCELERATION = 10
const PLAYER_SPEED = 3.8
const BOOST_FACTOR = 2.5
const BOOST_DURATION = 0.3
const BOOST_RESTORE = 0.5

var game: Game
var id := -1. # float because json
var rotation_ = 0.
var rotation_anim = 0.
var position_ = Vector2(0, 0)
var position_anim = Vector2(0, 0)
var boosting := false
var boost_pressed := false # only used in non-controllable players
var walking := false
var username: String

var movement_base: Node3D = Node3D.new()

var character: Character = preload("res://player/character/character.tscn").instantiate()
var chat_bubble: ChatBubble = null
var item_bubble: ItemBubble = null

var clear_chat_timer: Timer = Timer.new()
var clear_item_timer: Timer = Timer.new()

var is_despawning: bool = false

var hand = []
var hand_base
var character_style: Dictionary
var player_class: String
var is_customer: bool
var is_chef: bool
var current_item_message = null

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

var _anim_angle: float = 0.0

const DEFAULT_HAND_BASE_POSITION_CENTER: Vector3 = Vector3(0, .425, .4)
const DEFAULT_HAND_BASE_POSITION_LEFT: Vector3 = Vector3(.3, .425, .4)
const DEFAULT_HAND_BASE_POSITION_RIGHT: Vector3 = Vector3(-.3, .425, .4)

func _init(_id: int, name_: String, pos: Vector2, character_style_: Dictionary, player_class_: String, game_: Game):
	id = _id
	character_style = character_style_
	player_class = player_class_
	game = game_
	if name_ != "":
		name = name_
	username = name_

	for _x in range(Global.hand_count): hand.append(null)

	add_child(movement_base)
	movement_base.add_child(character)
	position_ = pos
	position_anim = pos
	movement_base.position = Vector3(pos.x, 0, pos.y)

	if Global.hand_count == 1:
		var center = Node3D.new()
		center.name = "HandBaseCenter"
		center.position = DEFAULT_HAND_BASE_POSITION_CENTER
		hand_base = [center]
	else:
		var left = Node3D.new()
		var right = Node3D.new()
		left.name = "HandBaseLeft"
		right.name = "HandBaseRight"
		left.position = DEFAULT_HAND_BASE_POSITION_LEFT
		right.position = DEFAULT_HAND_BASE_POSITION_RIGHT
		hand_base = [left, right]

	for h in hand_base:
		movement_base.add_child(h)

	clear_chat_timer.one_shot = true
	clear_item_timer.one_shot = true
	add_child(clear_chat_timer)
	add_child(clear_item_timer)

	is_customer = player_class == "customer"
	is_chef = player_class == "chef"

	movement_base.scale = Vector3.ONE * 0.0001

func _ready():
	character.set_style(character_style, player_class)

	clear_chat_timer.timeout.connect(clear_text_message)
	clear_item_timer.timeout.connect(clear_item_message)

	Settings.hook_changed_init("gameplay.usernames", self, update_username_tag)

func update_position(new_position: Vector2, new_direction: Vector2, new_rotation: float, new_boosting: bool):
	position_ = new_position
	rotation_ = new_rotation
	boost_pressed = new_boosting
	direction = new_direction

func update_movement(dt: float, boost: bool):
	# This function implements the server's movement system.
	# It may not be modified, so that it matches the server's logic.
	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 target_v = direction * PLAYER_SPEED * (BOOST_FACTOR if boosting else 1.)
	velocity_ = target_v + (velocity_ - target_v) * exp(-dt * PLAYER_ACCELERATION)
	position_ += velocity_ * dt
	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 aabb_point_distance(mi: Vector2, ma: Vector2, p: Vector2) -> float:
	# Calculates the signed distance of a point p to an axis aligned bounding box
	return (p - p.clamp(mi, ma)).length()

func update_username_tag(state):
	var tag: Label3D = character.username_tag
	tag.text = username
	tag.visible = state and is_chef

func set_item(i: Item, h: int):
	if hand[h] != null: hand[h].remove()
	# if i != null:
	# 	@warning_ignore("static_called_on_instance")
	# 	hand_base_position[h] = DEFAULT_HAND_BASE_POSITION_LEFT - Vector3(0.,i.height() * 0.5, 0.)
	# 	@warning_ignore("static_called_on_instance")
	# 	hand_base_position[1] = DEFAULT_HAND_BASE_POSITION_RIGHT - Vector3(0.,i.height() * 0.5, 0.)
	character.holding = i != null
	hand[h] = i
	if hand[h] != null: hand[h].owned_by = hand_base[h]

func remove_item(h: int):
	var i = hand[h]
	if i == null: push_error("holding nothing")
	hand[h] = null
	character.holding = false
	return i

func progress(position__: float, speed: float, warn: bool, h: int):
	if hand[h] != null: hand[h].progress(position__, speed, warn)

func finish(h: int):
	if hand[h] != null: hand[h].finish()

func take_item(tile: Tile, h: int):
	if hand[h] != null: push_error("already holding an item")
	var i = tile.take_item()
	i.take()
	set_item(i, h)

func put_item(tile: Tile, h: int):
	var i = remove_item(h)
	i.put()
	tile.put_item(i)

func pass_to(player: Player, hfrom: int, hto: int):
	var i = remove_item(hfrom)
	i.player_owned_timer = 0
	if player.hand[hto] != null:
		push_error("target is already holding an item")
	player.set_item(i, hto)

const MAX_DT = 1. / 50.
func _process(delta):
	for h in hand.size():
		if hand[h] != null:
			hand[h].rotation_target = hand_base[h].global_rotation.y
			hand[h].position_target = hand_base[h].global_position
	_anim_angle = fmod(_anim_angle + delta, TAU)
	position_anim = G.interpolate(position_anim, position_, delta * 20)
	rotation_anim = G.interpolate_angle(rotation_anim, rotation_, delta * 20)
	movement_base.position.x = position_anim.x
	movement_base.position.z = position_anim.y
	movement_base.rotation.y = rotation_anim
	walking = direction.length_squared() > 0.1
	character.walking = walking
	character.boosting = boosting
	if is_despawning:
		movement_base.scale /= exp(delta * 8)
		if movement_base.scale.length() < 0.01: self.queue_free()
	else:
		movement_base.scale = Vector3.ONE * G.interpolate(movement_base.scale.x, 1, delta * 8)
	
	while delta > 0.001:
		var dt = min(delta, MAX_DT)
		_process_movement(delta)
		delta -= dt

func _process_movement(dt):
	update_movement(dt, boost_pressed)

func item_message(item_name: String, timeout_initial: float, timeout_remaining: float):
	if item_bubble != null:
		item_bubble.queue_free()
	item_bubble = ITEM_BUBBLE.instantiate()
	movement_base.add_child(item_bubble)
	item_bubble.set_item(item_name, timeout_initial, timeout_remaining)
	clear_item_timer.start(timeout_remaining)
	current_item_message = item_name

func clear_item_message():
	if item_bubble != null:
		current_item_message = null
		item_bubble.queue_free()

func text_message(message: Game.TextMessage):
	if chat_bubble != null:
		chat_bubble.queue_free()
	chat_bubble = CHAT_BUBBLE.instantiate()
	movement_base.add_child(chat_bubble)
	chat_bubble.set_text(message.text)
	clear_chat_timer.start(message.timeout_remaining)

func clear_text_message():
	if chat_bubble != null:
		chat_bubble.queue_free()
