using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
using VOID.Generic.Dict.Ability;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Objects.Unit.Actions;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Player.Input;
using VOID.Generic.Player.Util;
using VOID.UserInterface;
using VOID.UserInterface.Game.Party;
using VOID.Generic.Util;
using VOID.ScriptableObjects.Abilities;
using PlayerInputManager = VOID.Generic.Player.Input.PlayerInputManager;

namespace VOID.Generic.Player
{
    /// <summary>
    /// When <see cref="PlayerController.activeUnit"/> starts <see cref="CastingAction"/> then player can select
    /// targets by <see cref="AbilityAimingType"/> selected in <see cref="BasicAbility"/>. After successfull selection
    /// and usage accepting then active unit starts <see cref="AbilityAction"/>.
    /// <br/>
    /// For each target <see cref="ProjectileVisualizer"/> is created.
    /// </summary>
    [DefaultExecutionOrder(-1)]
    public class PlayerAbilityCasting : MonoBehaviour
    {
        [SerializeField] private Material _trajectoryMaterial;
        [SerializeField] private Material _areaOfEffectMaterial;
        private UnitObject _caster;
        private BasicAbility _ability;
        private readonly List<GameObject> _targets = new();
        private bool _isTargetInRange = true;
        private bool _isValidTarget = true;

        private void Start()
        {
            PlayerController.current.OnActiveUnitChange += OnActiveUnitChange;
            CastingInputManager.current.confirm.performed += OnConfirm;
            CastingInputManager.current.cancel.performed += OnCancel;

            enabled = false;
        }
        
        private void FixedUpdate()
        {
            switch (_ability.aimingType)
            {
                case AbilityAimingType.Blank:
                    // Blank have zero targets - just do nothing
                    break;
                case AbilityAimingType.Self:
                    // Always one target - self
                    _targets.First().transform.position = _caster.transform.position;
                    break;
                case AbilityAimingType.Target:
                    // Default target aiming - player needs to select targets
                    MoveCurrentTargetToCursor();
                    RotateUnitTowardsFirstTarget();
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <summary>
        /// Stick current target to position which cursor pointing.
        /// </summary>
        private void MoveCurrentTargetToCursor()
        {
            // Zero targets should not be possible here thanks to validations in BasicAbility
            if (_targets.Count == 0)
            {
                Debug.LogError($"{AbilityAimingType.Target} with exactly ZERO targets defined in {_ability} is invalid!");
                return;
            }
            
            // Get position which mouse pointing, if nothing found then player poiting UI or literaly nothing?
            var select = SelectUtil.Select();
            var targetPosition = select.position;
            var targetObject = select.selectObject;
            if (!select.hit) return;
            if (select.onUI && select.unitPortraitUI == null) return;

            // Directional target correction - forces aim at maximum distance in given direction ignoring height
            // By default this correction can't aim at objects - it is purely positional
            if (_ability.aimingCorrectionType is AbilityAimingCorrectionType.Directional)
            {
                targetPosition = TargetPositionCorrection_Directional(targetPosition);
                targetObject = null;
            }
            
            // Target must be allowed by ability
            _isValidTarget = _ability.projectileSteps.First().validTargets.All(validTarget =>
                validTarget.IsValid(_caster, targetPosition, targetObject));
            
            // Target must be in range!
            _isTargetInRange = RangeUtil.IsInRange(_caster, targetPosition, _ability.range);
            
            // Player can have multiple target, but now we manage only latest one's position
            var currentTarget = _targets.Last();
            currentTarget.SetActive(_isValidTarget && _isTargetInRange);
            
            // Current target to cursor's pointing position
            currentTarget.transform.position = targetPosition;
        }

        private Vector3 TargetPositionCorrection_Directional(Vector3 toPosition)
        {
            var casterPosition = _caster.transform.position;
            toPosition = Vector3.Scale(toPosition, new Vector3(1, 0, 1));
            var fromPosition = Vector3.Scale(casterPosition, new Vector3(1, 0, 1));
            var direction = Vector3.Normalize(toPosition - fromPosition);
            return casterPosition + direction * (_ability.range + _caster.basicData.radius);
        }

        /// <summary>
        /// While selecting first target make <see cref="UnitObject"/> look it that direction.
        /// </summary>
        private void RotateUnitTowardsFirstTarget()
        {
            if (_targets.Count == 1) _caster.OrderInstantly(new RotateAction(_caster, _targets.Last().transform.position));
        }

        /// <summary>
        /// When player accepting this target's position. Also check if this position is valid!
        /// </summary>
        private void OnConfirm(InputAction.CallbackContext callbackContext)
        {
            // On UI allow only selecting unit via its portrait
            if (GameUI.current.isCursorOverUI && GameUI.current.PickTopComponentUI<UnitPortraitUI>() == null) return;
            
            // Not in player's unit range OR ability dont allow that kind of target
            if (!_isTargetInRange || !_isValidTarget) return;

            if (_targets.Count == _ability.castUsages) ConfirmAbility();
            else if (_targets.Count > 0) InitNextTarget();
        }

        /// <summary>
        /// Whole casting completed, proceed to start ability.
        /// </summary>
        private void ConfirmAbility()
        {
            // OrderStop() also will stop currently running CastingAction which also runs this::EndCasting()
            // Thats why we need to prepare AbilityAction before that
            var abilityAction = new AbilityAction(_caster, _ability, _targets.Select(target => target.transform.position));
            _caster.OrderStop();
            _caster.Order(abilityAction);
        }

        /// <summary>
        /// When player want to cancel current target or cancel whole casting.
        /// </summary>
        private void OnCancel(InputAction.CallbackContext callbackContext)
        {
            // Cancel current target if there is any (ability can be targetless and only need confirmation)
            if (_targets.Count > 0) UndoTarget();
            
            // Stops whole casting if there is no targeting active
            if (_targets.Count == 0) CancelAbility();
        }

        /// <summary>
        /// Cancel current target so it can go back to previous target selecting.
        /// </summary>
        private void UndoTarget()
        {
            var lastIndex = _targets.Count-1;
            Destroy(_targets.Last());
            _targets.RemoveAt(lastIndex);
        }

        /// <summary>
        /// Cancel whole casting process.
        /// </summary>
        private void CancelAbility()
        {
            EndCasting();
            _caster.OrderStop();
        }

        /// <summary>
        /// When player changes <see cref="PlayerController.activeUnit"/>:<br/>
        /// 1. Stops casting for previous unit if active<br/>
        /// 2. Waiting for new unit casting action
        /// </summary>
        private void OnActiveUnitChange(UnitObject unit)
        {
            if (_caster)
            {
                _caster.unitEvents.onStartCasting.RemoveListener(StartCasting);
                EndCasting();
            }
            _caster = unit;
            _caster.unitEvents.onStartCasting.AddListener(StartCasting);
        }

        /// <summary>
        /// When player order <see cref="PlayerController.activeUnit"/> to <see cref="CastingAction"/>.
        /// </summary>
        private void StartCasting(CastingEvent castingEvent)
        {
            PlayerInputManager.current.enabled = false;
            CastingInputManager.current.enabled = true;
            _ability = castingEvent.ability;
            _targets.Clear();
            enabled = true;
            InitNextTarget();
            
            _caster.unitEvents.onEndCasting.AddListener(EndCasting);
        }

        /// <summary>
        /// Prepares next target to be visualized on scene.
        /// </summary>
        private void InitNextTarget()
        {
            // No more targets to be visualized - usually because ability don't have any targets to select
            if (_ability.castUsages <= _targets.Count) return;
            
            var visualizerGameObject = new GameObject("AbilityTarget-" + _targets.Count);
            var projectileVisualizer = visualizerGameObject.AddComponent<ProjectileVisualizer>();

            projectileVisualizer.Init(
                _caster,
                _ability.projectileSteps,
                _trajectoryMaterial,
                _areaOfEffectMaterial,
                visualizerGameObject.transform
            );
            
            _targets.Add(visualizerGameObject);
        }

        /// <summary>
        /// When player order <see cref="PlayerController.activeUnit"/> to stop casting.
        /// </summary>
        private void EndCasting(CastingEvent castingEvent)
        {
            EndCasting();
        }
        
        /// <summary>
        /// Stop whole target selecting and projectile visualizing.
        /// </summary>
        private void EndCasting()
        {
            _caster.unitEvents.onEndCasting.RemoveListener(EndCasting);
            _targets.ForEach(Destroy);
            _targets.Clear();
            enabled = false;
            
            CastingInputManager.current.enabled = false;
            PlayerInputManager.current.enabled = true;
        }
    }
}