extends CharacterBody3D

# Member variables
const QUAKE = 0.0625 / 2
const g = -800 * QUAKE
const MAX_SPEED = 320 * QUAKE
const MAX_AIR_SPEED = 30 * QUAKE
const FRICTION = 4# * QUAKE
const STOP_SPEED = 100 * QUAKE
const ACCEL = 15# * QUAKE
const AIR_ACCEL = 2# * QUAKE
const FORWARD_SPEED = 200 * QUAKE
const SIDE_SPEED = 350 * QUAKE
const UP_SPEED = 270 * QUAKE
const JUMP_SPEED = 270 * QUAKE

@export
var interact_range: float = 5.0
@export
var hold_force = 400
@export
var max_hold_velocity: float = 1000.0

# const DEACCEL= 8
# const MAX_SLOPE_ANGLE = 30https://www.youtube.com/watch?v=v3zT3Z5apaM

@onready
var _camera = $CSGMesh3D/Camera3D
@onready
var _ball = $Ball
@onready
var _collider = $CollisionShape3D
@onready
var _flashlight: SpotLight3D = $Flashlight

var _paused = false
var _noclip = false

var _held_object: RigidBody3D
#var _held_object_distance: float


func _ready():
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)


func _process(_delta):
    # Do this on the render tick so it doesnt jitter at higher FPS
    _ball.global_position = _camera.global_position - _camera.global_transform.basis.z * interact_range


func _physics_process(delta):
    # Shamelessly stolen code from quake :)

    var fmove = 0.0
    var smove = 0.0
    var upmove = 0.0

    if Input.is_action_pressed("move_forward"):
        fmove += -FORWARD_SPEED
    if Input.is_action_pressed("move_backward"):
        fmove += FORWARD_SPEED
    if Input.is_action_pressed("move_left"):
        smove += -SIDE_SPEED
    if Input.is_action_pressed("move_right"):
        smove += SIDE_SPEED
    #if Input.is_action_pressed("up"):
    #	upmove += UP_SPEED
    if Input.is_action_pressed("run"):
            fmove *= 2.0
            smove *= 2.0

    var wishvel = Vector3()

    if _noclip:
        wishvel = _camera.global_basis.z * fmove + _camera.global_basis.x * smove
    else:
        wishvel = basis.z * fmove + basis.x * smove

    if _noclip:
        wishvel.y += upmove

    var wishdir = wishvel.normalized()
    var wishspeed = wishvel.length()

    if wishspeed > MAX_SPEED:
        wishvel *= MAX_SPEED / wishspeed
        wishspeed = MAX_SPEED

    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y += JUMP_SPEED

    var vel = velocity
    if _noclip:
        vel = wishvel * 2
    elif is_on_floor():
        vel = _apply_friction(vel, delta)
        vel = _apply_accel(vel, wishdir, wishspeed, delta)
    else:
        vel = _apply_air_accel(vel, wishvel, wishspeed, delta)

    if not _noclip and not is_on_floor():
        vel.y += g * delta

    velocity = vel
    move_and_slide()

    if _held_object:
        _apply_holding_force(delta)


func _apply_friction(vel: Vector3, delta: float):
    var speed = sqrt(vel.x ** 2 + vel.z ** 2)
    if(speed == 0.0): return vel

    # TODO: edge detect friction?
    var friction = FRICTION

    var control = 0.0
    if speed < STOP_SPEED:
        control = STOP_SPEED
    else:
        control = speed

    var newspeed = speed - delta * control * friction

    if newspeed < 0.0:
        newspeed = 0.0
    else:
        newspeed /= speed

    return vel * newspeed


func _apply_accel(vel: Vector3, wishdir: Vector3, wishspeed: float, delta: float):
    var currentspeed = vel.dot(wishdir)
    var addspeed = wishspeed - currentspeed
    if addspeed <= 0.0:
        return vel

    var accelspeed = ACCEL * delta * wishspeed
    if accelspeed > addspeed:
        accelspeed = addspeed

    return vel + accelspeed * wishdir


func _apply_air_accel(vel: Vector3, wishvel: Vector3, wishspeed: float, delta: float):
    var wishveloc = wishvel.normalized()
    var wishspd = wishvel.length()

    if wishspd > MAX_AIR_SPEED:
        wishspd = MAX_AIR_SPEED

    var currentspeed = vel.dot(wishveloc)
    var addspeed = wishspd - currentspeed
    if addspeed <= 0:
        return vel

    var accelspeed = AIR_ACCEL * wishspeed * delta
    if accelspeed > addspeed:
        accelspeed = addspeed

    return vel + accelspeed * wishveloc


func _apply_holding_force(delta):
    var hold_point = _camera.global_position - _camera.global_transform.basis.z * 3.0
    var delta_position = hold_point - _held_object.global_position
    var move_speed = min(delta_position.length() * hold_force, max_hold_velocity)
    _held_object.linear_velocity = delta_position.normalized() * move_speed * delta


func _input(event):
    if event.is_action_pressed("pause"):
        _paused = not _paused
        if _paused:
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
        else:
            Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

    if _paused: return


    if event.is_action_pressed('noclip'):
        _noclip = not _noclip
        if _noclip:
            _collider.disabled = true
        else:
            _collider.disabled = false

    if event.is_action_pressed('flashlight'):
        _flashlight.visible = not _flashlight.visible

    # Camera
    if event is InputEventMouseMotion:
        rotation.y += -event.relative.x * 0.005
        _camera.rotation.x += -event.relative.y * 0.005
        _camera.rotation.x = clamp(_camera.rotation.x, deg_to_rad(-90), deg_to_rad(90))

        # Rotate flashlight
        _flashlight.rotation.x = _camera.rotation.x

    if event.is_action_pressed("interact"):
        # Handle dropping things
        if _held_object:
            #_held_object.drop()
            _held_object = null
            return

        var space_state = get_world_3d().direct_space_state
        # use global coordinates, not local to node
        var ray_end = _camera.global_position - _camera.global_transform.basis.z * interact_range
        var query = PhysicsRayQueryParameters3D.create(_camera.global_position, ray_end)
        # Don't collide with ourselves
        query.exclude = [self]
        var result = space_state.intersect_ray(query)
        if result.has('collider'):
            var other: Node3D = result.collider
            if other.has_method('interact'):
                other.interact(self)
            if other.is_in_group('can_hold'):
                _held_object = other
                #_held_object_distance = (Vector3(result.position) - _camera.global_position).length()


func force_drop():
    if _held_object:
        _held_object = null
