class_name BuggedVehicle
extends VehicleBody

signal speed_updated(speed_kph, speed_percent)
signal rpm_updated(rpm, rpm_percent)
signal throttle_updated(throttle_percent)
signal brake_updated(brake_percent)
signal clutch_updated(clutch_percent)
signal gear_updated(gear)
signal steering_updated(steering_angle, steering_percent)
signal position_updated(player_id, position)

enum GearRequest { NONE, UP, DOWN }

export(float) var max_steer_angle = 25
export(float) var speed_steer_angle = 10
export(float) var max_steer_speed = 100.0
export(float) var max_steer_input = 80.0

export(float) var max_engine_force = 85.0
export(float) var max_brake_force = 50.0
export(float) var throttle_power = 6000.0
export(float) var max_rpm_loss_ps = 3000.0
export(float) var base_engine_pitch = 0.5
export(float) var expected_max_speed = 200

export(Array) var gear_ratios = [3.4, 2.5, 2.0, 1.5, 1.25]
export(float) var reverse_ratio = -3
export(float) var final_drive = 3.45
export(float) var max_rpm = 3500
export(float) var min_rpm = 900
export(float) var gear_switch_time = 0.2
export(Curve) var power_curve = null
export(Curve) var sound_curve = null

export(float) var automatic_gear_up_threshold = 0.8
export(float) var automatic_gear_down_threshold = 0.3

var clutch_position: float = 0.0
var rpm = 0
var gear = 1

var gear_timer = 0

var traction_wheels: Array
var reset_transform: Transform = Transform.IDENTITY

var inputs: VehicleInputs = VehicleInputs.new()

onready var frwheel: VehicleWheel = $front_right
onready var flwheel: VehicleWheel = $front_left
onready var rrwheel: VehicleWheel = $rear_right
onready var rlwheel: VehicleWheel = $rear_left
onready var frsmoke: TireSmoke = $fr_tire_smoke
onready var flsmoke: TireSmoke = $fl_tire_smoke
onready var rrsmoke: TireSmoke = $rr_tire_smoke
onready var rlsmoke: TireSmoke = $rl_tire_smoke

onready var engine_sound_player: AudioStreamPlayer3D = $engine_sound_player
onready var engine_sound_playback: AudioStreamPlayback = $engine_sound_player.get_stream_playback()

onready var max_steer_angle_rad: float = deg2rad(max_steer_angle)
onready var speed_steer_angle_rad: float = deg2rad(speed_steer_angle)
onready var max_steer_input_rad: float = deg2rad(max_steer_input)

onready var auto_clutch_rpm_limit = max_rpm * automatic_gear_down_threshold


class VehicleInputs:
	var gear_request = GearRequest.NONE
	var clutch = 0.0
	var throttle = 0.0
	var brake = 0.0
	var handbrake = 0.0
	# Left is positive, right is negative
	var steering = 0.0

	func to_array() -> Array:
		return [gear_request, clutch, throttle, brake, handbrake, steering]

	func from_array(array: Array) -> void:
		gear_request = array[0]
		clutch = array[1]
		throttle = array[2]
		brake = array[3]
		handbrake = array[4]
		steering = array[5]


func _ready():
	for wheel in [frwheel, flwheel, rrwheel, rlwheel]:
		if wheel.use_as_traction:
			traction_wheels.append(wheel)

	_generate_engine_sound(0)
	engine_sound_player.play()


func get_cockpit_position() -> Node:
	return $cockpit


func get_hood_position() -> Node:
	return $hood


func get_bumper_position() -> Node:
	return $bumper


func get_static_follow_position() -> Node:
	return $static_follow


func _integrate_forces(state: PhysicsDirectBodyState) -> void:
	if reset_transform != Transform.IDENTITY:
		state.linear_velocity = Vector3.ZERO
		state.angular_velocity = Vector3.ZERO
		state.set_transform(reset_transform)
		reset_transform = Transform.IDENTITY


func _get_gear_ratio():
	if gear == 0:
		return 0
	elif gear == -1:
		return reverse_ratio
	else:
		return gear_ratios[gear - 1]


func _handle_gear_switch(delta: float):
	if gear_timer > 0:
		gear_timer = max(0, gear_timer - delta)
	if clutch_position > 0.8 or GlobalSettings.auto_clutch or GlobalSettings.automatic_transmission:
		match inputs.gear_request:
			GearRequest.UP:
				_gear_up()
			GearRequest.DOWN:
				_gear_down()


func _gear_up():
	if gear + 1 <= gear_ratios.size():
		gear += 1
		gear_timer = gear_switch_time * (2 - clutch_position)
		emit_signal("gear_updated", gear)


func _gear_down():
	if gear - 1 >= -1:
		gear -= 1
		gear_timer = gear_switch_time * (2 - clutch_position)
		emit_signal("gear_updated", gear)


func _has_traction():
	for wheel in traction_wheels:
		if wheel.is_in_contact():
			return true
	return false


func _update_wheels_smoke():
	for wheelnsmoke in [
		[frwheel, frsmoke], [flwheel, flsmoke], [rrwheel, rrsmoke], [rlwheel, rlsmoke]
	]:
		wheelnsmoke[1].update(wheelnsmoke[0].get_skidinfo())


func _lerp_rpm(from, to, delta, factor):
	var new_val = lerp(from, to, factor)
	if abs(from - new_val) > max_rpm_loss_ps * delta:
		var new_factor = inverse_lerp(
			from, to, from - sign(from - new_val) * max_rpm_loss_ps * delta
		)
		new_val = lerp(from, to, new_factor)
	return new_val


func _physics_process(delta: float):
	_update_wheels_smoke()
	clutch_position = inputs.clutch
	_handle_gear_switch(delta)
	var throttle = inputs.throttle
	var brake_input = inputs.brake

	var wheel_rpm = traction_wheels[0].get_rpm()
	var speed = wheel_rpm * 2.0 * PI * rrwheel.wheel_radius / 60.0 * 3600.0 / 1000.0

	if GlobalSettings.automatic_transmission and speed < 1 and gear == 1 and brake_input > 0.1:
		_gear_down()
		_gear_down()

	if GlobalSettings.automatic_transmission and speed > -1 and gear == -1 and throttle > 0.1:
		_gear_up()
		_gear_up()

	if GlobalSettings.automatic_transmission and gear == -1:
		var swap = throttle
		throttle = brake_input
		brake_input = swap

	if GlobalSettings.auto_clutch or GlobalSettings.automatic_transmission:
		clutch_position = 1 - min(rpm, auto_clutch_rpm_limit) / auto_clutch_rpm_limit

	if gear_timer > 0:
		clutch_position = 1
		throttle = 0

	var final_rpm = abs(wheel_rpm) * final_drive
	var transmission_rpm = final_rpm * abs(_get_gear_ratio())

	if gear != 0:
		rpm = lerp(rpm, transmission_rpm, delta * 10 * (1 - clutch_position))
		rpm = _lerp_rpm(rpm, min_rpm, delta, delta * clutch_position)
	else:
		rpm = _lerp_rpm(rpm, min_rpm, delta, delta)
	if _has_traction():
		rpm += throttle * delta * (max(clutch_position, 1 if gear == 0 else 0)) * throttle_power
	else:
		rpm += throttle * delta * throttle_power
	rpm = clamp(rpm, 0, max_rpm)
	var rpm_factor = clamp(rpm / max_rpm, 0.0, 1.0)
	var power_factor = power_curve.interpolate_baked(rpm_factor)

	_generate_engine_sound(rpm_factor)

	# Transfer to transmission
	var transmission_input = power_factor * (1 - clutch_position) * _get_gear_ratio()

	var final_input = transmission_input * final_drive

	emit_signal("throttle_updated", throttle)
	emit_signal("brake_updated", brake_input)
	emit_signal("clutch_updated", clutch_position)
	brake = brake_input * max_brake_force
	engine_force = throttle * final_input * max_engine_force

	var handbrake = inputs.handbrake
	rrwheel.brake = handbrake * max_brake_force
	rlwheel.brake = handbrake * max_brake_force

	if GlobalSettings.automatic_transmission and rpm_factor > automatic_gear_up_threshold:
		_gear_up()
	elif GlobalSettings.automatic_transmission and rpm_factor < automatic_gear_down_threshold:
		if gear > 1:
			_gear_down()

	emit_signal("speed_updated", speed, speed / expected_max_speed)
	emit_signal("rpm_updated", rpm, rpm_factor)

	var steering_input = inputs.steering

	var steer_speed_factor = clamp(speed / max_steer_speed, 0.0, 1.0)

	steering = steering_input * lerp(max_steer_angle_rad, speed_steer_angle_rad, steer_speed_factor)
	emit_signal("steering_updated", steering, steering / max_steer_angle_rad)

	emit_signal("position_updated", get_network_master(), global_transform)

	if MultiplayerController.is_online():
		if get_network_master() == get_tree().get_network_unique_id():
			_synchronize()


func _generate_engine_sound(rpm_factor):
	engine_sound_player.pitch_scale = base_engine_pitch + 2 * rpm_factor
	var to_fill = engine_sound_playback.get_frames_available()
	var factor = rpm_factor
	if to_fill <= 0:
		return
	var fill_segment = 1.0 / to_fill
	var fill_percent = 0.0
	while to_fill > 0:
		engine_sound_playback.push_frame(Vector2(1.0, 1.0) * factor)
		factor += (
			cos(factor)
			* sin(factor)
			* (1 + to_fill % 2)
			* ((sound_curve.interpolate_baked(fill_percent) - 0.5) * 2)
		)
		to_fill -= 1
		fill_percent += fill_segment


func _synchronize():
	rpc("sync_position", transform)
	rpc("sync_inputs", inputs.to_array())


remote func sync_position(position: Transform):
	reset_transform = position
	emit_signal("position_updated", get_network_master(), global_transform)


remote func sync_inputs(remote_inputs):
	inputs.from_array(remote_inputs)
