# 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 FollowCamera
extends Camera3D

const ROTATE_SPEED: float = 1.5
const ROTATE_WEIGHT: float = 8.0
const ROTATE_UP_SPEED: float = 0.7
const ROTATE_UP_WEIGHT: float = 4.0
const ANGLE_UP_MIN: float = 0.5
const ANGLE_UP_MAX: float = 0.4999 * PI
const LOOK_WEIGHT: float = 8.0
const MOVE_WEIGHT: float = 2.0
const ZOOM_WEIGHT_AUTO: float = 0.5
const ZOOM_SPEED: float = 1.0
const DISCRETE_DURATION: float = 0.3
const ZOOM_WEIGHT: float = 10.0
const MAX_ZOOM: float = 20.0
const MIN_ZOOM: float = 2.0
const ZOOM_CURRENT_TO_MAX_RATIO: float = 1.35

@export var target: Node3D

var autozoom: bool = false
var angle_target: float = 0
var angle: float = 0
var angle_up_target: float = 1
var angle_up: float = 1
var ground: Vector3
var camera_distance: float = 10.
var camera_distance_target: float = camera_distance

var _disable_input := false

func _ready():
	if target == null:
		push_warning("target is not set")
	else:
		ground = target.position

func _process(delta):
	if target != null:
		handle_input(delta)
		follow(delta)

func _input(event):
	if _disable_input: return
	if Input.is_action_just_pressed("reset"):
		reset()
	if event is InputEventMagnifyGesture:
		zoom(1 - event.factor)

func reset():
	angle_target = 0
	angle_up_target = 1
	camera_distance_target = 10

func zoom(zoom_dist: float) -> void:
	camera_distance_target *= exp(zoom_dist * ZOOM_SPEED)
	camera_distance_target = clamp(camera_distance_target, MIN_ZOOM, MAX_ZOOM)

func handle_input(delta):
	if _disable_input: return
	var invert_factor = -1 if Settings.read("gameplay.invert_camera") else 1;
	angle_target += Input.get_axis("rotate_left", "rotate_right") * ROTATE_SPEED * delta * invert_factor
	angle_up_target += Input.get_axis("rotate_down", "rotate_up") * ROTATE_UP_SPEED * delta * invert_factor
	
	var zoom_dist = Input.get_axis("zoom_in", "zoom_out") * delta
	zoom_dist += float(Input.is_action_just_pressed("zoom_out_discrete")) * DISCRETE_DURATION
	zoom_dist -= float(Input.is_action_just_pressed("zoom_in_discrete")) * DISCRETE_DURATION
	zoom(zoom_dist)

func jump_to_target():
	follow(INF)

func follow(delta):
	if Settings.read("gameplay.first_person"):
		var player = target.get_parent()
		if not player is ControllablePlayer: return
		fov = 80.
		global_position = target.global_position + Vector3.UP * 1.33
		rotation.x = player.fps_look.x
		rotation.y = -player.fps_look.y
		if target.get_parent() is ControllablePlayer:
			target.get_parent().input_rotation = -rotation.y
		return
	else: fov = 45.

	angle = G.interpolate_angle(angle, angle_target, delta * ROTATE_WEIGHT)

	angle_up_target = clamp(angle_up_target, ANGLE_UP_MIN, ANGLE_UP_MAX)
	angle_up = G.interpolate_angle(angle_up, angle_up_target, delta * ROTATE_UP_WEIGHT)

	var offset = Vector3(0, sin(angle_up) * camera_distance, cos(angle_up) * camera_distance).rotated(Vector3.UP, angle)

	var new_transform = transform
	new_transform.origin = target.position + offset
	new_transform = new_transform.looking_at(target.position)

	if Settings.read("gameplay.interpolate_camera_rotation"):
		transform.basis = Basis.from_euler(Vector3(
			G.interpolate_angle(transform.basis.get_euler().x, new_transform.basis.get_euler().x, delta * LOOK_WEIGHT),
			G.interpolate_angle(transform.basis.get_euler().y, new_transform.basis.get_euler().y, delta * LOOK_WEIGHT),
			G.interpolate_angle(transform.basis.get_euler().z, new_transform.basis.get_euler().z, delta * LOOK_WEIGHT)
		))
	else:
		transform.basis = new_transform.basis

	# add 0.5, this is the head height
	ground = G.interpolate(ground, target.position + Vector3(0., 0.5, 0.), delta * MOVE_WEIGHT)

	camera_distance = G.interpolate(camera_distance, camera_distance_target, delta * (ZOOM_WEIGHT_AUTO if autozoom else ZOOM_WEIGHT))

	position = ground + offset
	if target.get_parent() is ControllablePlayer:
		target.get_parent().input_rotation = -angle_target

func set_ingame(state: bool, _in_lobby: bool):
	# Disable input in lobby
	if state: reset()
