using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
using VOID.Generic.AreaOfEffect;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Projectile;
using VOID.Generic.Trajectory;

namespace VOID.Generic.Player
{
    /// <summary>
    /// Visualizes <see cref="ProjectileObject"/> by displaying <see cref="AbstractTrajectory"/> and <see cref="AbstractAreaOfEffect"/>.<br/>
    /// TODO: So far only first step of projectile is supported!
    /// </summary>
    [DefaultExecutionOrder(-1)]
    public class ProjectileVisualizer : MonoBehaviour
    {
        private UnitObject _caster;
        private ProjectileStep _projectileStep;
        private AbstractTrajectory _trajectory;
        private Transform _transformStart;
        private Transform _transformEnd;
        
        private Vector3 _positionStart;
        private Vector3 _positionEnd;
        private Vector3 _lastPositionStart;
        private Vector3 _lastPositionEnd;
        private Vector3 _collisionOnPosition;

        private LineRenderer _lineRenderer;
        private MeshFilter _meshFilter;
        private MeshRenderer _meshRenderer;

        private GameObject _visualizerGameObject;

        private void Awake() => enabled = false;

        /// <summary>
        /// Initializes and starts projectile visualizing.
        /// </summary>
        /// <param name="caster">UnitObject who is casting this projectile</param>
        /// <param name="projectileSteps">Definition how projectile will move and hit</param>
        /// <param name="trajectoryMaterial">Material to draw Trajectory</param>
        /// <param name="areaOfEffectMaterial">Material to draw AreaOfEffect</param>
        /// <param name="transformEnd">Target position for projectile</param>
        public void Init(
            UnitObject caster,
            ProjectileStep[] projectileSteps,
            Material trajectoryMaterial,
            Material areaOfEffectMaterial,
            Transform transformEnd
        )
        {
            // TODO: So far only first step of projectile is supported!
            _projectileStep = projectileSteps.FirstOrDefault();
            if (_projectileStep == default) return;

            _caster = caster;
            _trajectory = _projectileStep.trajectory.Clone();
            _transformStart = FindStartTransform(_projectileStep);
            _transformEnd = transformEnd;

            // GameObject containing meshes of Trajectory and AreaOfEffect
            _visualizerGameObject = new GameObject("Projectile Visualizer");
            _visualizerGameObject.transform.parent = gameObject.transform;
            _visualizerGameObject.transform.rotation =
                _projectileStep.areaOfEffect.GetRotation(_transformStart.position, _transformEnd.position);
            
            // Trajectory visualizing
            if (_trajectory is not InstantTrajectory)
            {
                _lineRenderer = _visualizerGameObject.AddComponent<LineRenderer>();
                _lineRenderer.enabled = false;
                _lineRenderer.startWidth = _projectileStep.radius;
                _lineRenderer.endWidth = _projectileStep.radius;
                _lineRenderer.shadowCastingMode = ShadowCastingMode.Off;
                _lineRenderer.sharedMaterial = trajectoryMaterial;
            }
            
            // AreaOfEffect visualizing
            if (_projectileStep.areaOfEffect is not NoneAreaOfEffect)
            {
                _meshFilter = _visualizerGameObject.AddComponent<MeshFilter>();
                _meshFilter.mesh = _projectileStep.areaOfEffect.GetMesh();
                _meshRenderer = _visualizerGameObject.AddComponent<MeshRenderer>();
                _meshRenderer.enabled = false;
                _meshRenderer.shadowCastingMode = ShadowCastingMode.Off;
                _meshRenderer.sharedMaterial = areaOfEffectMaterial;
            }

            enabled = true;
        }

        private void FixedUpdate()
        {
            _positionStart = _transformStart.position;
            _positionEnd = _transformEnd.position;
            
            // If positions didn't change then there is no need to recalculate whole Trajectory and AreaOfEffect
            if (_positionStart == _lastPositionStart && _positionEnd == _lastPositionEnd) return;

            _lastPositionStart = _positionStart;
            _lastPositionEnd = _positionEnd;
            
            UpdateTrajectory();
            UpdateAreaOfEffect();
        }

        /// <summary>
        /// Updates whole <see cref="AbstractTrajectory"/>
        /// </summary>
        private void UpdateTrajectory()
        {
            // Its instant, that means there is no trajectory
            if (_trajectory is InstantTrajectory) return;
            
            _trajectory.Init(_positionStart, _positionEnd);
            var pointsNeeded = Mathf.FloorToInt(_trajectory.trajectoryDistance / _projectileStep.speed / Time.fixedDeltaTime);
            var points = _trajectory.GetPositions(pointsNeeded);

            // Predict collision on trajectory
            points = GetPointsToFirstCollision(points);
            
            _lineRenderer.positionCount = points.Length;
            _lineRenderer.SetPositions(points);
            _lineRenderer.enabled = true;
        }

        /// <summary>
        /// If <see cref="ProjectileCollisionType"/> specified then try to detect collision on trajectory.
        /// </summary>
        private Vector3[] GetPointsToFirstCollision(Vector3[] points)
        {
            int layerMask;
            switch (_projectileStep.collisionType)
            {
                case ProjectileCollisionType.StaticOrDynamicCollision:
                    layerMask = LayerManager.Static | LayerManager.StaticHideable | LayerManager.Dynamic;
                    break;
                case ProjectileCollisionType.StaticCollision:
                    layerMask = LayerManager.Static | LayerManager.StaticHideable;
                    break;
                default:
                    return points;
            }

            // To prevent hitting caster and ground under caster ignore everything in caster's radius
            var skip = 0;
            if (_projectileStep.startFrom is ProjectileStepStartFrom.FromCaster)
                skip = Array.FindIndex(points, point => Vector3.Distance(point, _positionStart) >= _caster.basicData.radius);
            if (skip == - 1) skip = 0;
            
            // Check if projectile will hit anything on its trajeactory (also prevent hitting caster)
            var collisionIndex = Array.FindIndex(points, skip, point =>
                Physics.OverlapSphere(point, _projectileStep.radius, layerMask)
                    .Any(collider => collider.GetComponentInParent<UnitObject>() != _caster));
            if (collisionIndex == - 1) collisionIndex = points.Length - 1;
            
            _collisionOnPosition = points[collisionIndex];
            return points.Take(collisionIndex + 1).ToArray();
        }

        /// <summary>
        /// Updates whole <see cref="AbstractAreaOfEffect"/>
        /// </summary>
        private void UpdateAreaOfEffect()
        {
            if (_projectileStep.areaOfEffect is NoneAreaOfEffect) return;
                
            _visualizerGameObject.transform.position = _collisionOnPosition != default ? _collisionOnPosition : _positionEnd;
            _visualizerGameObject.transform.rotation = _projectileStep.areaOfEffect.GetRotation(_positionStart, _positionEnd);
            _meshRenderer.enabled = true;
        }

        /// <summary>
        /// Returns start position for current step by defined <see cref="ProjectileStepStartFrom"/>.
        /// </summary>
        private Transform FindStartTransform(ProjectileStep step)
        {
            return step.startFrom switch
            {
                ProjectileStepStartFrom.FromStart => _caster.unitData.castPosition,
                ProjectileStepStartFrom.FromCaster => _caster.transform,
                ProjectileStepStartFrom.FromHit => _caster.unitData.castPosition,
                _ => throw new ArgumentOutOfRangeException(
                    nameof(step.startFrom), step.startFrom, null)
            };
        }
    }
}