class_name Raycast extends Node3D

class RaycastHit extends RefCounted:
    var collider: CollisionObject3D
    var shape: int
    var normal: Vector3
    var position: Vector3
    var face_index: int

    @warning_ignore("shadowed_variable")
    func _init(collider: CollisionObject3D, shape: int, normal: Vector3, position: Vector3, index: int) -> void:
        self.collider = collider
        self.shape = shape
        self.normal = normal
        self.position = position
        self.face_index = index

    func get_shape_owner() -> Option:
        if typeof(self.collider) == TYPE_NIL or self.shape == 0: return Option.none
        var owner_id = self.collider.shape_find_owner(shape)
        return Option.some(self.collider.shape_owner_get_owner(owner_id))

    static func from_dict(hit: Dictionary) -> Option:
        if hit.is_empty(): return Option.none
        return Option.some(
            RaycastHit.new(
                hit['collider'],
                hit['shape'],
                hit['normal'],
                hit['position'],
                hit['face_index']
            )
        )

@export var enabled: bool = true
@export var exclude_parent: bool = true
@export var length: float = 1

@export_flags_3d_physics var collision_mask: int = 1

@export var hit_from_inside: bool = false
@export var hit_backfaces: bool = true

@export_group("Collide With")
@export var areas: bool = false
@export var bodies: bool = true

@export var exclusions: Array[CollisionObject3D] = []
var _exclusions: Array

@export_group("Debug Shape")
@export var custom_color: Color
@export var width: int = 2

var forward: Vector3:
    get: return -global_basis.z

var hit: Option = Option.none

func _ready() -> void:
    set_physics_process(enabled)
    _exclusions = exclusions.map(func(x): return x.get_rid())
    if exclude_parent:
        match _find_parent_collider():
            var x when x.is_some():
                _exclusions.append(x.unwrap().get_rid())

func _physics_process(_delta: float) -> void:
    force_raycast_update()

func _find_parent_collider(node: Node = self) -> Option:
    if typeof(node) == TYPE_NIL: return Option.none
    if is_instance_of(node, SubViewport):
        return Option.none
    if is_instance_of(node, CollisionObject3D):
        return Option.some(node)
    else:
        return _find_parent_collider(node.get_parent())

func force_raycast_update() -> void:
    var to = forward * length + global_position
    var query = PhysicsRayQueryParameters3D.create(global_position, to, collision_mask, _exclusions)
    DebugDraw3D.draw_ray(global_position, forward, length, custom_color, .25)
    query.hit_from_inside = hit_from_inside
    query.hit_back_faces = hit_backfaces
    query.collide_with_areas = areas
    query.collide_with_bodies = bodies
    var raycast_hit = get_world_3d().direct_space_state.intersect_ray(query)
    hit = RaycastHit.from_dict(raycast_hit)

func is_colliding() -> bool:
    return hit.is_some()
