using System;
using System.Collections.Generic;
using UnityEngine.EventSystems;

namespace UnityEngine.UI
{
    [Serializable]
    public class TransitionData
    {
        public Graphic targetGraphic;
        public Selectable.Transition transition = Selectable.Transition.ColorTint;
        public ColorBlock colors = ColorBlock.defaultColorBlock;
        public SpriteState spriteState;
        public AnimationTriggers animationTriggers = new AnimationTriggers();
    }


    [ExecuteAlways]
    [SelectionBase]
    [DisallowMultipleComponent]
    public class UXSelectable : UIBehaviour,
        IMoveHandler,
        IPointerDownHandler, IPointerUpHandler,
        IPointerEnterHandler, IPointerExitHandler,
        ISelectHandler, IDeselectHandler
    {
        [Serializable]
        public enum SelectionState
        {
            Normal,
            Highlighted,
            Pressed,
            Selected,
            Disabled,
        }

        #region Fields

        [SerializeField] protected UXNavigation m_Navigation = UXNavigation.defaultNavigation;

        [SerializeField] protected bool m_Interactable = true;
        [SerializeField] protected TransitionData m_MainTransition = new TransitionData();

        // current visual / logical selection state (now in base)
        [SerializeField] protected SelectionState m_SelectionState = SelectionState.Normal;

        // runtime event flags (used by currentSelectionState)
        protected bool isPointerInside { get; set; }
        protected bool isPointerDown { get; set; }
        protected bool hasSelection { get; set; }

        protected bool m_GroupsAllowInteraction = true;
        private readonly List<CanvasGroup> m_CanvasGroupCache = new List<CanvasGroup>();

        // registry for navigation find functions
        protected static UXSelectable[] s_Selectables = new UXSelectable[10];
        protected static int s_SelectableCount = 0;
        protected int m_CurrentIndex = -1;
        protected bool m_EnableCalled = false;
        [SerializeField] private Animator _animator;
        public Animator Animator => _animator ? _animator : _animator = GetComponent<Animator>();

        public static readonly Dictionary<string, int> _animTriggerCache = new()
        {
            { "Normal", Animator.StringToHash("Normal") },
            { "Highlighted", Animator.StringToHash("Highlighted") },
            { "Pressed", Animator.StringToHash("Pressed") },
            { "Selected", Animator.StringToHash("Selected") },
            { "Disabled", Animator.StringToHash("Disabled") },
        };


        public Graphic targetGraphic
        {
            get { return m_MainTransition.targetGraphic; }
            set
            {
                if (SetPropertyUtility.SetClass(ref m_MainTransition.targetGraphic, value)) OnSetProperty();
            }
        }

        #endregion

        #region Properties

        public UXNavigation navigation
        {
            get { return m_Navigation; }
            set
            {
                if (SetPropertyUtility.SetStruct(ref m_Navigation, value))
                    OnSetProperty();
            }
        }

        public bool Interactable
        {
            get => m_Interactable;
            set
            {
                if (m_Interactable == value) return;
                m_Interactable = value;
                OnSetProperty();
            }
        }

        public static UXSelectable[] allSelectablesArray
        {
            get
            {
                UXSelectable[] temp = new UXSelectable[s_SelectableCount];
                Array.Copy(s_Selectables, temp, s_SelectableCount);
                return temp;
            }
        }

        #endregion

        #region Unity Lifecycle

        protected override void Awake()
        {
            base.Awake();
            // nothing specific here; subclasses may initialize visuals
        }

        protected override void OnEnable()
        {
            if (m_EnableCalled)
                return;

            base.OnEnable();

            if (s_SelectableCount == s_Selectables.Length)
            {
                UXSelectable[] temp = new UXSelectable[s_Selectables.Length * 2];
                Array.Copy(s_Selectables, temp, s_Selectables.Length);
                s_Selectables = temp;
            }

            if (EventSystem.current && EventSystem.current.currentSelectedGameObject == gameObject)
            {
                hasSelection = true;
            }

            m_CurrentIndex = s_SelectableCount;
            s_Selectables[m_CurrentIndex] = this;
            s_SelectableCount++;
            isPointerDown = false;
            m_GroupsAllowInteraction = ParentGroupAllowsInteraction();

            m_EnableCalled = true;

            // Ensure visual state matches current settings immediately
            OnSetProperty();
        }

        protected override void OnDisable()
        {
            if (!m_EnableCalled)
                return;

            s_SelectableCount--;

            s_Selectables[s_SelectableCount].m_CurrentIndex = m_CurrentIndex;
            s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount];
            s_Selectables[s_SelectableCount] = null;

            InstantClearState();

            base.OnDisable();
            m_EnableCalled = false;
        }

        protected void OnCanvasGroupChanged()
        {
            var parentGroupAllows = ParentGroupAllowsInteraction();
            if (parentGroupAllows != m_GroupsAllowInteraction)
            {
                m_GroupsAllowInteraction = parentGroupAllows;
                OnSetProperty();
            }
        }

        #endregion

        #region Groups & Interactable

        protected bool ParentGroupAllowsInteraction()
        {
            Transform t = transform;
            while (t != null)
            {
                t.GetComponents(m_CanvasGroupCache);
                for (var i = 0; i < m_CanvasGroupCache.Count; i++)
                {
                    if (m_CanvasGroupCache[i].enabled && !m_CanvasGroupCache[i].interactable)
                        return false;
                    if (m_CanvasGroupCache[i].ignoreParentGroups)
                        return true;
                }

                t = t.parent;
            }

            return true;
        }

        public virtual bool IsInteractable()
        {
            return m_GroupsAllowInteraction && m_Interactable;
        }

        #endregion

        #region Property change handling & SelectionState API

        /// <summary>
        /// Called when a property that affects visuals changes (navigation/interactable/animation/etc).
        /// </summary>
        protected virtual void OnSetProperty()
        {
            // If not interactable => Disabled state
            if (!IsInteractable())
            {
                m_SelectionState = SelectionState.Disabled;
                DoStateTransition(SelectionState.Disabled, false);
            }
            else
            {
                // If previously disabled, restore to Normal
                if (m_SelectionState == SelectionState.Disabled)
                    m_SelectionState = SelectionState.Normal;

                // Apply current selection state
                DoStateTransition(m_SelectionState, false);
            }
        }

        /// <summary>
        /// Computed selection state based on pointer / selection flags and interactability.
        /// Mirrors Unity's Selectable.currentSelectionState behavior.
        /// </summary>
        protected SelectionState currentSelectionState
        {
            get
            {
                if (!IsInteractable())
                    return SelectionState.Disabled;
                if (isPointerDown)
                    return SelectionState.Pressed;
                if (hasSelection)
                    return SelectionState.Selected;
                if (isPointerInside)
                    return SelectionState.Highlighted;
                return SelectionState.Normal;
            }
        }

        /// <summary>
        /// Evaluate and transition to the computed selection state. Call after changing pointer/selection flags.
        /// </summary>
        protected void EvaluateAndTransitionToSelectionState()
        {
            if (!IsActive() || !IsInteractable())
                return;

            DoStateTransition(currentSelectionState, false);
        }

        /// <summary>
        /// Force apply visual state immediately.
        /// </summary>
        protected void ForceSetState(SelectionState state)
        {
            m_SelectionState = state;
            DoStateTransition(state, true);
        }

        /// <summary>
        /// Clear internal state and restore visuals (used on disable / focus lost).
        /// </summary>
        protected virtual void InstantClearState()
        {
            // reset flags
            isPointerInside = false;
            isPointerDown = false;
            hasSelection = false;

            // restore visuals based on main transition's normal state
            if (m_MainTransition == null) return;

            switch (m_MainTransition.transition)
            {
                case Selectable.Transition.ColorTint:
                    TweenColor(m_MainTransition, m_MainTransition.colors.normalColor * m_MainTransition.colors.colorMultiplier, true);
                    break;
                case Selectable.Transition.SpriteSwap:
                    SwapSprite(m_MainTransition, null);
                    break;
                case Selectable.Transition.Animation:
                    PlayAnimation(m_MainTransition.animationTriggers.normalTrigger);
                    break;
            }

            m_SelectionState = SelectionState.Normal;
        }

        /// <summary>
        /// Core mapping from SelectionState -> TransitionData and execution.
        /// Subclasses can override PlayAnimation or ApplyVisualState for custom behavior.
        /// </summary>
        /// <param name="state"></param>
        /// <param name="instant"></param>
        protected virtual void DoStateTransition(SelectionState state, bool instant)
        {
            if (!gameObject.activeInHierarchy)
                return;

            if (m_MainTransition == null)
                return;

            Color tintColor = Color.white;
            Sprite sprite = null;
            string trigger = null;

            switch (state)
            {
                case SelectionState.Normal:
                    tintColor = m_MainTransition.colors.normalColor;
                    sprite = m_MainTransition.spriteState.highlightedSprite;
                    trigger = m_MainTransition.animationTriggers.normalTrigger;
                    break;
                case SelectionState.Highlighted:
                    tintColor = m_MainTransition.colors.highlightedColor;
                    sprite = m_MainTransition.spriteState.highlightedSprite;
                    trigger = m_MainTransition.animationTriggers.highlightedTrigger;
                    break;
                case SelectionState.Pressed:
                    tintColor = m_MainTransition.colors.pressedColor;
                    sprite = m_MainTransition.spriteState.pressedSprite;
                    trigger = m_MainTransition.animationTriggers.pressedTrigger;
                    break;
                case SelectionState.Selected:
                    tintColor = m_MainTransition.colors.selectedColor;
                    sprite = m_MainTransition.spriteState.selectedSprite;
                    trigger = m_MainTransition.animationTriggers.selectedTrigger;
                    break;
                case SelectionState.Disabled:
                    tintColor = m_MainTransition.colors.disabledColor;
                    sprite = m_MainTransition.spriteState.disabledSprite;
                    trigger = m_MainTransition.animationTriggers.disabledTrigger;
                    break;
            }

            // Execute appropriate transition type
            switch (m_MainTransition.transition)
            {
                case Selectable.Transition.ColorTint:
                    TweenColor(m_MainTransition, tintColor * m_MainTransition.colors.colorMultiplier, instant);
                    break;
                case Selectable.Transition.SpriteSwap:
                    SwapSprite(m_MainTransition, sprite);
                    break;
                case Selectable.Transition.Animation:
                    PlayAnimation(trigger);
                    break;
                case Selectable.Transition.None:
                default:
                    break;
            }

            // keep serialized state in sync
            m_SelectionState = state;
        }

        #endregion

        #region Navigation helpers

        public UXSelectable FindSelectable(Vector3 dir)
        {
            dir = dir.normalized;
            Vector3 localDir = Quaternion.Inverse(transform.rotation) * dir;
            Vector3 pos = transform.TransformPoint(GetPointOnRectEdge(transform as RectTransform, localDir));
            float maxScore = Mathf.NegativeInfinity;
            float maxFurthestScore = Mathf.NegativeInfinity;
            float score = 0;

            bool wantsWrapAround = navigation.wrapAround && (m_Navigation.mode == UXNavigation.Mode.Vertical || m_Navigation.mode == UXNavigation.Mode.Horizontal);

            UXSelectable bestPick = null;
            UXSelectable bestFurthestPick = null;

            for (int i = 0; i < s_SelectableCount; ++i)
            {
                UXSelectable sel = s_Selectables[i];

                if (sel == this)
                    continue;

                if (!sel.IsInteractable() || sel.navigation.mode == UXNavigation.Mode.None)
                    continue;

#if UNITY_EDITOR
                if (Camera.current != null && !UnityEditor.SceneManagement.StageUtility.IsGameObjectRenderedByCamera(sel.gameObject, Camera.current))
                    continue;
#endif

                var selRect = sel.transform as RectTransform;
                Vector3 selCenter = selRect != null ? (Vector3)selRect.rect.center : Vector3.zero;
                Vector3 myVector = sel.transform.TransformPoint(selCenter) - pos;

                float dot = Vector3.Dot(dir, myVector);

                if (wantsWrapAround && dot < 0)
                {
                    score = -dot * myVector.sqrMagnitude;
                    if (score > maxFurthestScore)
                    {
                        maxFurthestScore = score;
                        bestFurthestPick = sel;
                    }

                    continue;
                }

                if (dot <= 0)
                    continue;

                score = dot / myVector.sqrMagnitude;

                if (score > maxScore)
                {
                    maxScore = score;
                    bestPick = sel;
                }
            }

            if (wantsWrapAround && null == bestPick) return bestFurthestPick;

            return bestPick;
        }

        private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir)
        {
            if (rect == null) return Vector3.zero;
            if (dir != Vector2.zero) dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y));
            dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f);
            return dir;
        }

        public virtual UXSelectable FindSelectableOnLeft()
        {
            if (m_Navigation.mode == UXNavigation.Mode.Explicit)
                if (m_Navigation.selectOnLeft != null && m_Navigation.selectOnLeft.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
                    return m_Navigation.selectOnLeft;

            if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0)
                return FindSelectable(transform.rotation * Vector3.left);
            return null;
        }

        public virtual UXSelectable FindSelectableOnRight()
        {
            if (m_Navigation.mode == UXNavigation.Mode.Explicit)
                if (m_Navigation.selectOnRight != null && m_Navigation.selectOnRight.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
                    return m_Navigation.selectOnRight;
            if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0)
                return FindSelectable(transform.rotation * Vector3.right);
            return null;
        }

        public virtual UXSelectable FindSelectableOnUp()
        {
            if (m_Navigation.mode == UXNavigation.Mode.Explicit)
                if (m_Navigation.selectOnUp != null && m_Navigation.selectOnUp.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
                    return m_Navigation.selectOnUp;
            if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0)
                return FindSelectable(transform.rotation * Vector3.up);
            return null;
        }

        public virtual UXSelectable FindSelectableOnDown()
        {
            if (m_Navigation.mode == UXNavigation.Mode.Explicit)
                if (m_Navigation.selectOnDown != null && m_Navigation.selectOnDown.IsInteractable() && m_Navigation.mode != UXNavigation.Mode.None)
                    return m_Navigation.selectOnDown;
            if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0)
                return FindSelectable(transform.rotation * Vector3.down);
            return null;
        }

        public virtual void OnMove(AxisEventData eventData)
        {
            switch (eventData.moveDir)
            {
                case MoveDirection.Right:
                    Navigate(eventData, FindSelectableOnRight());
                    break;
                case MoveDirection.Up:
                    Navigate(eventData, FindSelectableOnUp());
                    break;
                case MoveDirection.Left:
                    Navigate(eventData, FindSelectableOnLeft());
                    break;
                case MoveDirection.Down:
                    Navigate(eventData, FindSelectableOnDown());
                    break;
            }
        }

        void Navigate(AxisEventData eventData, UXSelectable sel)
        {
            if (sel != null && sel.IsActive())
                eventData.selectedObject = sel.gameObject;
        }

        #endregion

        #region Pointer / Select base implementations (update flags + evaluate)

        public virtual void OnPointerDown(PointerEventData eventData)
        {
            if (eventData.button != PointerEventData.InputButton.Left)
                return;

            if (IsInteractable() && navigation.mode != UXNavigation.Mode.None && EventSystem.current != null)
                EventSystem.current.SetSelectedGameObject(gameObject, eventData);

            isPointerDown = true;
            EvaluateAndTransitionToSelectionState();
        }

        public virtual void OnPointerUp(PointerEventData eventData)
        {
            if (eventData.button != PointerEventData.InputButton.Left)
                return;

            isPointerDown = false;
            EvaluateAndTransitionToSelectionState();
        }

        public virtual void OnPointerEnter(PointerEventData eventData)
        {
            isPointerInside = true;
            EvaluateAndTransitionToSelectionState();
        }

        public virtual void OnPointerExit(PointerEventData eventData)
        {
            isPointerInside = false;
            EvaluateAndTransitionToSelectionState();
        }

        public virtual void OnSelect(BaseEventData eventData)
        {
            hasSelection = true;
            EvaluateAndTransitionToSelectionState();
        }

        public virtual void OnDeselect(BaseEventData eventData)
        {
            hasSelection = false;
            EvaluateAndTransitionToSelectionState();
        }

        #endregion

        #region Visual transition helpers (main transition + low level ops)

        /// <summary>
        /// High-level API: apply main transition visuals (child classes can override, UXButton overrides to add child transitions).
        /// </summary>
        /// <param name="state"></param>
        /// <param name="instant"></param>
        public virtual void ApplyVisualState(SelectionState state, bool instant)
        {
            if (m_MainTransition != null)
                ApplyTransition(m_MainTransition, state, instant);
        }

        /// <summary>
        /// Apply a single TransitionData (handles ColorTint / SpriteSwap / Animation).
        /// Animation triggering calls PlayAnimation (virtual).
        /// </summary>
        protected void ApplyTransition(TransitionData data, SelectionState state, bool instant)
        {
            if (data == null) return;
            if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation)
                return;

            Color color = Color.white;
            Sprite sprite = null;
            string trigger = null;

            switch (state)
            {
                case SelectionState.Normal:
                    color = data.colors.normalColor;
                    sprite = data.spriteState.highlightedSprite;
                    trigger = data.animationTriggers.normalTrigger;
                    break;
                case SelectionState.Highlighted:
                    color = data.colors.highlightedColor;
                    sprite = data.spriteState.highlightedSprite;
                    trigger = data.animationTriggers.highlightedTrigger;
                    break;
                case SelectionState.Pressed:
                    color = data.colors.pressedColor;
                    sprite = data.spriteState.pressedSprite;
                    trigger = data.animationTriggers.pressedTrigger;
                    break;
                case SelectionState.Selected:
                    color = data.colors.selectedColor;
                    sprite = data.spriteState.selectedSprite;
                    trigger = data.animationTriggers.selectedTrigger;
                    break;
                case SelectionState.Disabled:
                    color = data.colors.disabledColor;
                    sprite = data.spriteState.disabledSprite;
                    trigger = data.animationTriggers.disabledTrigger;
                    break;
            }

            switch (data.transition)
            {
                case Selectable.Transition.ColorTint:
                    TweenColor(data, color * data.colors.colorMultiplier, instant);
                    break;
                case Selectable.Transition.SpriteSwap:
                    SwapSprite(data, sprite);
                    break;
                case Selectable.Transition.Animation:
                    PlayAnimation(trigger);
                    break;
            }
        }

        protected void TweenColor(TransitionData data, Color color, bool instant)
        {
            if (data == null || data.targetGraphic == null) return;
            data.targetGraphic.CrossFadeColor(
                color,
                instant ? 0f : data.colors.fadeDuration,
                true,
                true
            );
        }

        protected static void SwapSprite(TransitionData data, Sprite sprite)
        {
            if (data == null) return;
            if (data.targetGraphic is Image img)
                img.overrideSprite = sprite;
        }

        /// <summary>
        /// Subclasses override this to trigger Animator triggers, etc.
        /// </summary>
        /// <param name="trigger"></param>
        protected virtual void PlayAnimation(string trigger)
        {
            if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger) || !gameObject.activeInHierarchy)
                return;

            foreach (int id in _animTriggerCache.Values)
                Animator.ResetTrigger(id);

            if (_animTriggerCache.TryGetValue(trigger, out int hash))
                Animator.SetTrigger(hash);
        }

        #endregion
    }
}
