using UnityEngine;

namespace ToolsBox.Raycast
{ 
    public class Raycast3D : MonoBehaviour
    {
        public enum RaycastShape
        {
            Line, Box, Sphere
        }

        public enum RaycastDirection
        {
            Up, Down, Right, Left, Forward, Backward
        }

        //Settings
        [SerializeField, Tooltip("The shape of the raycast")] RaycastShape _shape;
        public RaycastShape Shape { get => _shape; }
        [SerializeField, Tooltip("Use raycast on Update")] bool _useOnUpdate;
        [SerializeField, Tooltip("Raycast detect multiple objects")] bool _useCastAll;
        [SerializeField, Tooltip("Max objects detectable")] int _maxCastToSave = 0;
        public bool UseCastAll { get => _useCastAll; }

        //Results

        [field: SerializeField, Tooltip("Raycast as hit")] public bool IsHitting { get; private set; }
        [field: SerializeField, Tooltip("Raycast hit result")] public RaycastHit Hit { get; private set; }
        [field: SerializeField, Tooltip("Raycast hits result")] public RaycastHit[] Hits { get; private set; }
        public int HitCounts { get; private set; }

        //Line
        [SerializeField, Tooltip("Line settings : Origin offset")] Vector3 _lineOriginOffset;
        [SerializeField, Tooltip("Line settings : line direction")] RaycastDirection _lineDirection;
        [SerializeField, Tooltip("Line settings : line size")] float _lineDistance;
        [SerializeField, Tooltip("Line settings : Layers to detect")] LayerMask _lineLayerMask;
        //Box
        [SerializeField, Tooltip("Box settings : Origin Offset")] Vector3 _boxOriginOffset;
        [SerializeField, Tooltip("Box settings : Box direction")] RaycastDirection _boxDirection;
        [SerializeField, Tooltip("Box settings : Box size from origin to side")] Vector3 _boxHalfSize;
        [SerializeField, Tooltip("Box settings : Distance from Origin")] float _boxDistance;
        [SerializeField, Tooltip("Box settings : Layers to detect")] LayerMask _boxLayerMask;
        [SerializeField, Tooltip("Box settings : Box rotation")] Vector3 _boxRotation;
        //Sphere
        [SerializeField, Tooltip("Sphere settings : Origin offset")] Vector3 _sphereOriginOffset;
        [SerializeField, Tooltip("Sphere settings : Sphere Direction")] RaycastDirection _sphereDirection;
        [SerializeField, Tooltip("Sphere settings : Distance from origin")] float _sphereDistance;
        [SerializeField, Tooltip("Sphere settings : Layers to detect")] LayerMask _sphereLayerMask;
        [SerializeField, Tooltip("Sphere settings : Radius")] float _sphereRadius;

        [SerializeField, Tooltip("Show Gizmos")] bool _debugEnabled;

        private void Awake()
        {
            if (_useCastAll)
            {
                Hits = new RaycastHit[_maxCastToSave];
            }
        }

        public bool ShootRaycast(out RaycastHit hit)
        {
            Vector3 origin;
            bool hasHit = false;
            hit = new RaycastHit();
            switch (_shape) { 
                case (RaycastShape.Line):
                    origin = transform.position + _lineOriginOffset;
                    hasHit = Physics.Raycast(origin, GetVectorFromDirection(_lineDirection), out hit, _lineDistance, _lineLayerMask, QueryTriggerInteraction.UseGlobal);
                    break;
                case (RaycastShape.Box):
                    origin = transform.position + _boxOriginOffset;
                    hasHit = Physics.BoxCast(origin, _boxHalfSize, GetVectorFromDirection(_boxDirection), out hit, Quaternion.Euler(_boxRotation), _boxDistance, _boxLayerMask, QueryTriggerInteraction.UseGlobal);
                    break;
                case (RaycastShape.Sphere):
                    origin = transform.position + _sphereOriginOffset;
                    hasHit = Physics.SphereCast(origin, _sphereRadius, GetVectorFromDirection(_sphereDirection), out hit, _sphereDistance, _sphereLayerMask, QueryTriggerInteraction.UseGlobal);
                    break;
            }
            return hasHit;
        }

        public int ShootRaycastAll(RaycastHit[] externalBuffer)
        {
            if (externalBuffer == null || externalBuffer.Length == 0) return 0;

            Vector3 origin;
            switch (_shape)
            {
                case RaycastShape.Line:
                    origin = transform.position + _lineOriginOffset;
                    return Physics.RaycastNonAlloc(origin, GetVectorFromDirection(_lineDirection), externalBuffer, _lineDistance, _lineLayerMask, QueryTriggerInteraction.UseGlobal);
                case RaycastShape.Box:
                    origin = transform.position + _boxOriginOffset;
                    return Physics.BoxCastNonAlloc(origin, _boxHalfSize, GetVectorFromDirection(_boxDirection), externalBuffer, Quaternion.Euler(_boxRotation), _boxDistance, _boxLayerMask, QueryTriggerInteraction.UseGlobal);
                case RaycastShape.Sphere:
                    origin = transform.position + _sphereOriginOffset;
                    return Physics.SphereCastNonAlloc(origin, _sphereRadius, GetVectorFromDirection(_sphereDirection), externalBuffer, _sphereDistance, _sphereLayerMask, QueryTriggerInteraction.UseGlobal);
                default:
                    return 0;
            }
        }

        public int ShootRaycastAll()
        {
            Vector3 origin;
            int hitCount = 0;
            switch (_shape)
            {
                case (RaycastShape.Line):
                    origin = transform.position + _lineOriginOffset;
                    hitCount = Physics.RaycastNonAlloc(origin, GetVectorFromDirection(_lineDirection), Hits, _lineDistance, _lineLayerMask, QueryTriggerInteraction.UseGlobal);
                    break;
                case (RaycastShape.Box):
                    origin = transform.position + _boxOriginOffset;
                    hitCount = Physics.BoxCastNonAlloc(origin, _boxHalfSize, GetVectorFromDirection(_boxDirection), Hits, Quaternion.Euler(_boxRotation), _boxDistance, _boxLayerMask, QueryTriggerInteraction.UseGlobal);
                    break;
                case (RaycastShape.Sphere):
                    origin = transform.position + _sphereOriginOffset;
                    hitCount = Physics.SphereCastNonAlloc(origin, _sphereRadius, GetVectorFromDirection(_sphereDirection), Hits, _sphereDistance, _sphereLayerMask, QueryTriggerInteraction.UseGlobal);
                    break;
                default: Hits = System.Array.Empty<RaycastHit>(); break;
            }
            return hitCount;
        }


        void Update()
        {
            if (_useOnUpdate)
            {
                if (_useCastAll)
                {
                    HitCounts = ShootRaycastAll();
                    IsHitting = HitCounts > 0;
                    //Debug.Log($"{IsHitting} : {hitCounts}");
                }
                else
                {
                    IsHitting = ShootRaycast(out RaycastHit hit);
                    Hit = hit;
                    //Debug.Log($"{IsHitting} : {(Hit.collider != null ? Hit.collider.gameObject.name : string.Empty)}");
                }
            }
        }

        Vector3 GetVectorFromDirection(RaycastDirection direction) {
            switch (direction) {
                case (RaycastDirection.Up): return transform.up;
                case (RaycastDirection.Down): return -transform.up; 
                case (RaycastDirection.Right): return transform.right; 
                case (RaycastDirection.Left): return -transform.right; 
                case (RaycastDirection.Forward): return transform.forward; 
                case (RaycastDirection.Backward): return -transform.forward; 
                default: return Vector3.zero;
            }
        }

        void OnDrawGizmos()
        {
            if(!_debugEnabled) return;
            if (_useOnUpdate && IsHitting) Gizmos.color = Color.green;
            else Gizmos.color = Color.red;
            switch (_shape) {
                case (RaycastShape.Line): Gizmos.DrawRay(transform.position + _lineOriginOffset, GetVectorFromDirection(_lineDirection) * _lineDistance); break;
                case (RaycastShape.Box): DrawBoxGizmo(); break;
                case (RaycastShape.Sphere): DrawSphereGizmo(); break;
            }
        }

        void DrawBoxGizmo()
        {
            Vector3 direction = GetVectorFromDirection(_boxDirection).normalized;
            Quaternion rotation = Quaternion.Euler(_boxRotation);
            Vector3 fullSize = _boxHalfSize * 2;

            Gizmos.color = Color.red;
            Gizmos.matrix = Matrix4x4.TRS(transform.position + _boxOriginOffset, rotation, Vector3.one);
            Gizmos.DrawWireCube(Vector3.zero, fullSize);

            Gizmos.color = Color.green;
            Vector3 endCenter = transform.position + _boxOriginOffset + direction * _boxDistance;
            Gizmos.matrix = Matrix4x4.TRS(endCenter, rotation, Vector3.one);
            Gizmos.DrawWireCube(Vector3.zero, fullSize);

            Gizmos.matrix = Matrix4x4.identity;
            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(transform.position + _boxOriginOffset, endCenter);
        }

        void DrawSphereGizmo()
        {
            Vector3 sphereOrigin = transform.position + _sphereOriginOffset;
            Vector3 sphereDirection = GetVectorFromDirection(_sphereDirection).normalized;
            Vector3 sphereEndCenter = sphereOrigin + sphereDirection * _sphereDistance;

            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(sphereOrigin, _sphereRadius);

            Gizmos.color = Color.green;
            Gizmos.DrawWireSphere(sphereEndCenter, _sphereRadius);

            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(sphereOrigin, sphereEndCenter);
        }
    }
}
