using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VOID.Generic.Objects.Item;
using VOID.Generic.Player.DraggingOperation;
using VOID.Generic.Player.Input;
using VOID.Generic.Player.Util;
using VOID.UserInterface;
using VOID.UserInterface.Game;
using VOID.Generic.Util;
using VOID.ScriptableObjects.Abilities;

namespace VOID.Generic.Player
{
    [DefaultExecutionOrder(-1)]
    public class DraggingManager : MonoBehaviour
    {
        public static DraggingManager current {get; private set;}

        // References
        private PlayerController _playerController;
        private PlayerInputManager _playerInputManager;
        private GameUI _gameUI;
        private DraggingUI _draggingUI;

        // Configuration
        [SerializeField] private float startDraggingAfter = 0.2f;
        
        // Private properties
        private List<AbstractDraggingOperation> _draggingOperations;
        private SelectUtilResult _sourceSelect;
        private SelectUtilResult _targetSelect;
        private bool _isDragging;
        private bool _isPreparing;

        // Submodule that visualize player's dragging
        private DraggingVisualizer _draggingVisualizer;
        
        // Transform from those GameObjects used as start and end for trajectory
        private GameObject _draggingFrom;
        private GameObject _draggingTo;

        private void Awake()
        {
            current = this;
            var thisTransform = transform;
            _draggingFrom = new GameObject("Dragging From");
            _draggingFrom.transform.parent = thisTransform;
            _draggingTo = new GameObject("Dragging To");
            _draggingTo.transform.parent = thisTransform;

            _draggingOperations = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(assembly => assembly.GetTypes())
                .Where(type => type.IsSubclassOf(typeof(AbstractDraggingOperation)) && type.IsClass)
                .Select(Activator.CreateInstance)
                .Cast<AbstractDraggingOperation>()
                .ToList();
        }

        private void Start()
        {
            _playerInputManager = PlayerInputManager.current;
            _playerInputManager.OnDraggingPrepare += PrepareDragging;
            _playerInputManager.OnDraggingStart += StartDragging;
            _playerInputManager.OnDraggingEnd += EndDragging;
            _playerController = PlayerController.current;
            _gameUI = GameUI.current;
            _draggingUI = DraggingUI.current;
        }

        private void FixedUpdate()
        {
            Dragging();
        }

        /// <summary>
        /// Checks that the operation can be performed and prepares the data for use
        /// </summary>
        private void PrepareDragging()
        {
            if (_isPreparing || _isDragging) Clean();
            var sourceSelect = SelectUtil.Select();

            // Dragging only when ability or item selected (and can be dragged)
            if (!sourceSelect.selectObject && !sourceSelect.selectAbility) return;
            if (sourceSelect.selectObject is not null and not ItemObject) return;
            var selectItem = (ItemObject)sourceSelect.selectObject;
            if (selectItem && !selectItem.itemData.canBeMoved) return;

            _draggingFrom.transform.position = sourceSelect.position;
            _draggingTo.transform.position = sourceSelect.position;
            _sourceSelect = sourceSelect;

            _isPreparing = true;
        }

        /// <summary>
        /// Initiate draging visualisation
        /// Requied: PrepapreDragging to be done before
        /// </summary>
        private void StartDragging()
        {
            if(!_isPreparing) return;
            _isDragging = true;

            if(_sourceSelect.selectObject is ItemObject) StartDraggingItem((ItemObject)_sourceSelect.selectObject);
            else if(_sourceSelect.selectAbility) StartDraggingAbility(_sourceSelect.selectAbility);
        }

        private void StartDraggingItem(ItemObject item)
        {
            // Prepare for Scene dragging
            _draggingVisualizer = DraggingVisualizer.Create(_draggingFrom.transform, _draggingTo.transform, item);
            
            // Prepare for UI dragging
            _draggingUI.Set(item);
        }

        private void StartDraggingAbility(BasicAbility ability)
        {
            _draggingUI.Set(ability);
            _draggingUI.Show();
        }
        
        private void Dragging()
        {
            if (!_isDragging) return;
            if (_sourceSelect.selectObject && !_gameUI.isCursorOverUI) DraggingItemOnScene();
            else if (_sourceSelect.selectObject && _gameUI.isCursorOverUI) DraggingItemOnUI();
            else if (_sourceSelect.selectAbility) DraggingAbility();
        }

        private void DraggingItemOnScene()
        {
            _draggingUI.Hide();
            var hit = SelectUtil.SelectHit();
            _draggingTo.transform.position = hit.point;

            // Dragging only onto STATIC objects (like floor)
            if (hit.collider.gameObject.layer is not LayerManager.StaticIndex)
            {
                _draggingVisualizer.Hide();
                return;
            }
            
            var isInRange = RangeUtil.IsInRange(
                _draggingFrom.transform.position,
                _draggingTo.transform.position,
                _playerController.activeUnit.unitData.takeRange*2,
                false
            );

            // Dragging only when in player's unit range (2 * take range)
            if (!isInRange)
            {
                _draggingVisualizer.Hide();
                return;
            }
            
            _draggingVisualizer.Show();
        }

        private void DraggingItemOnUI()
        {
            _draggingUI.Show();
            _draggingVisualizer.Hide();
        }

        private void DraggingAbility()
        {
        }

        /// <summary>
        /// This function completes the transfer process. It contains the main logic responsible for moving an object from one place to another
        /// Required: StartDragging to be done before
        /// </summary>
        private void EndDragging()
        {
            if (!_isDragging)
            {
                Clean();
                return;
            }

            _targetSelect = SelectUtil.Select();

            if (_gameUI.isCursorOverUI && _targetSelect.slotUI?.slotType == null)
            {
                Debug.LogWarning("Dragged into invalid place over UI");
                Clean();
                return;
            }

            var operation = _draggingOperations.FirstOrDefault(op => op.Check(_sourceSelect, _targetSelect));
            if (operation != null)
            {
                operation.Perform(_playerController.activeUnit, _sourceSelect, _targetSelect);
                Clean();
                return;
            }

            Debug.LogWarning("Dragging between unsupported places: " +
                             $"'{_sourceSelect.slotUI?.slotType}' -> '{_targetSelect.slotUI?.slotType}'");
            Clean();
        }

        private void Clean()
        {
            if (_draggingVisualizer)
            {
                Destroy(_draggingVisualizer.gameObject);
                _draggingVisualizer = null;
            }
            _isDragging = false;
            _isPreparing = false;
            _sourceSelect = null;
            _targetSelect = null;
            _draggingUI.Hide();
        }
    }
}