class_name OverworldCamera extends Camera3D

enum CameraMode { FREE, CENTERED }

# const COLLISION_MARGIN:float = 0.1
const COLLISION_MARGIN:float = 0

@export_category("Target")
@export var targetNode:Node3D = null
@export var pivotOffset:Vector3 = Vector3(0, 1.2, 0)

@export_category("Orbit")
@export var distance:float = 7.0
@export var minDistance:float = 2.5
@export var orbitSensitivity:float = 144.0
@export var orbitAcceleration:float = 600.0
@export var orbitFriction:float = 10.0
@export var pitchMin:float = -80.0
@export var pitchMax:float = 70.0

@export_category("Mouse")
@export var mouseSensitivity:float = 0.3

@export_category("Auto-center")
@export var centeredDelay:float = 1.0
@export var centeredFollowRate:float = 0.75
@export var centeredMaxFollowRate:float = 1.0
@export var centeredPitch:float = 30.0

@export_category("Collision")
@export_flags_3d_physics var collisionMask:int = 1

var _mode:CameraMode = CameraMode.CENTERED
var _yaw:float = 0.0
var _pitch:float = 30.0
var _camVelocity:Vector2 = Vector2.ZERO
var _freeTimer:float = 0.0
var _mouseDelta:Vector2 = Vector2.ZERO
var _rightMouseHeld:bool = false

func _input(event:InputEvent) -> void:
  if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
    _rightMouseHeld = event.pressed
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _rightMouseHeld else Input.MOUSE_MODE_VISIBLE
  elif event is InputEventMouseMotion and _rightMouseHeld:
    _mouseDelta += event.relative

func _process(delta:float) -> void:
  if targetNode == null:
    return

  var xMult:float = -1.0 if SETTINGS.invertCameraX else 1.0
  var yMult:float = 1.0 if SETTINGS.invertCameraY else -1.0

  var orbitInput:Vector2 = Input.get_vector(
    "camera_orbit_left", "camera_orbit_right",
    "camera_orbit_up", "camera_orbit_down"
  )

  var controllerActive:bool = orbitInput.length() > 0.01
  var mouseActive:bool = _mouseDelta.length_squared() > 0.0

  # Any manual camera input returns to FREE and resets the centering timer
  if controllerActive or mouseActive:
    _mode = CameraMode.FREE
    _freeTimer = 0.0

  # Controller input: ramp up to target speed, then friction-dampen on release
  if controllerActive:
    _camVelocity = _camVelocity.move_toward(
      orbitInput * orbitSensitivity * SETTINGS.cameraSpeedController, orbitAcceleration * delta
    )
  else:
    _camVelocity = _camVelocity.lerp(Vector2.ZERO, minf(orbitFriction * delta, 1.0))
  _yaw += _camVelocity.x * delta * xMult
  _pitch += _camVelocity.y * delta * yMult

  # Right-click mouse drag
  if mouseActive:
    _yaw += _mouseDelta.x * mouseSensitivity * SETTINGS.cameraSpeedMouse * xMult
    _pitch += _mouseDelta.y * mouseSensitivity * SETTINGS.cameraSpeedMouse * yMult
    _mouseDelta = Vector2.ZERO

  # center_camera input → switch to CENTERED immediately
  if Input.is_action_just_pressed("center_camera"):
    _mode = CameraMode.CENTERED

  # In FREE mode, accumulate time toward auto-centering while the player is moving
  if _mode == CameraMode.FREE:
    var playerBody := targetNode as CharacterBody3D
    if playerBody != null and playerBody.velocity.length_squared() > 0.1:
      _freeTimer += delta
      if _freeTimer >= centeredDelay:
        _mode = CameraMode.CENTERED
        _freeTimer = 0.0
    else:
      _freeTimer = 0.0

  # In CENTERED mode, smoothly slerp toward behind the player while they move;
  # rate scales up with angular distance so large offsets catch up quickly.
  if _mode == CameraMode.CENTERED:
    var centerBody := targetNode as CharacterBody3D
    if centerBody != null and centerBody.velocity.length_squared() > 0.1:
      var behindYaw:float = rad_to_deg(atan2(
        targetNode.global_transform.basis.z.x,
        targetNode.global_transform.basis.z.z
      ))
      var centerYawDiff:float = fposmod(behindYaw - _yaw + 180.0, 360.0) - 180.0
      var totalAngle:float = abs(centerYawDiff) + abs(centeredPitch - _pitch)
      var dynamicRate:float = minf(centeredFollowRate * (1.0 + totalAngle / 90.0), centeredMaxFollowRate)
      var t:float = minf(dynamicRate * delta, 1.0)
      _pitch = lerpf(_pitch, centeredPitch, t)
      _yaw += centerYawDiff * t

  _pitch = clamp(_pitch, pitchMin, pitchMax)

  var pivot:Vector3 = targetNode.global_transform.origin + pivotOffset
  var yawRad:float = deg_to_rad(_yaw)
  var pitchRad:float = deg_to_rad(_pitch)
  var dir:Vector3 = Vector3(
    sin(yawRad) * cos(pitchRad),
    sin(pitchRad),
    cos(yawRad) * cos(pitchRad)
  )

  var finalPos:Vector3 = _resolvePosition(pivot, dir)
  global_transform.origin = finalPos
  look_at(pivot, Vector3.UP)

  # Lerp _yaw/_pitch toward the actual constrained position so input resumes
  # smoothly, and small per-frame raycast variations don't oscillate.
  var actualDir:Vector3 = (finalPos - pivot).normalized()
  var targetPitch:float = rad_to_deg(asin(clamp(actualDir.y, -1.0, 1.0)))
  var targetYaw:float = rad_to_deg(atan2(actualDir.x, actualDir.z))
  var feedbackRate:float = minf(120.0 * delta, 1.0)
  _pitch = lerpf(_pitch, targetPitch, feedbackRate)
  var feedbackYawDiff:float = fposmod(targetYaw - _yaw + 180.0, 360.0) - 180.0
  _yaw += lerpf(0.0, feedbackYawDiff, feedbackRate)

func _resolvePosition(pivot:Vector3, dir:Vector3) -> Vector3:
  var desired:Vector3 = pivot + dir * distance
  var space:PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
  var query:PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(pivot, desired)
  query.collision_mask = collisionMask
  var hit:Dictionary = space.intersect_ray(query)

  if hit.is_empty():
    return desired

  var hitNormal:Vector3 = hit["normal"]
  var hitDist:float = (hit["position"] - pivot).length() - COLLISION_MARGIN

  # Sphere-plane circle intersection: find the closest valid point that
  # satisfies both minDistance from pivot and stays clear of terrain.
  var targetDist:float = maxf(hitDist, minDistance)
  var d:float = hitNormal.dot(pivot - hit["position"]) - COLLISION_MARGIN
  var rSq:float = targetDist * targetDist - d * d
  if rSq <= 0.0:
    return pivot - hitNormal * d

  var circleCenter:Vector3 = pivot - hitNormal * d
  var circleRadius:float = sqrt(rSq)

  var toDesired:Vector3 = desired - circleCenter
  toDesired -= hitNormal * hitNormal.dot(toDesired)
  if toDesired.length_squared() < 0.0001:
    return circleCenter

  return circleCenter + toDesired.normalized() * circleRadius
