using System.Collections.Generic;
using Jovian.PopupSystem.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;

namespace Jovian.PopupSystem {
    /// <summary>
    /// Behavior class for a popup view. Manages the generic grow-only element cache,
    /// positioning, visibility, and layout. Operates on a <see cref="PopupReference"/>
    /// MonoBehaviour for scene references.
    /// </summary>
    public sealed class PopupView {
        readonly PopupReference reference;
        readonly PopupSettings settings;

        // Generic element cache: keyed by element type
        readonly Dictionary<PopupElementType, List<GameObject>> elementCache = new();
        readonly Dictionary<PopupElementType, int> elementIndex = new();

        // Positioning state
        PopupPositionMode positionMode;
        RectTransform anchorTarget;
        AnchorSide anchorSide;
        Vector2 followOffset;
        float screenEdgePadding;
        float maxWidth;
        bool isVisible;
        bool layoutDirty;
        Canvas rootCanvas;
        Camera canvasCamera;

        /// <summary>The underlying MonoBehaviour reference holder.</summary>
        public PopupReference Reference => reference;

        /// <summary>The CanvasGroup for animation control.</summary>
        public CanvasGroup CanvasGroup => reference.CanvasGroup;

        /// <summary>The content RectTransform where elements are parented.</summary>
        public RectTransform Content => reference.Content;

        /// <summary>The root Transform of the popup GameObject.</summary>
        public Transform Transform => reference.transform;

        /// <summary>Whether the popup is currently visible.</summary>
        public bool IsVisible => isVisible;

        /// <summary>
        /// Creates a new popup view wrapping the given reference and settings.
        /// </summary>
        public PopupView(PopupReference reference, PopupSettings settings) {
            this.reference = reference;
            this.settings = settings;
        }

        // --- Element cache (generic, grow-only) ---

        /// <summary>
        /// Returns the next available cached element for the given type, or instantiates
        /// a new one from the settings prefab registry. The element is activated and placed
        /// at the end of the content layout.
        /// </summary>
        public GameObject GetElement(PopupElementType elementType) {
            if(!elementCache.TryGetValue(elementType, out var cache)) {
                cache = new List<GameObject>();
                elementCache[elementType] = cache;
                elementIndex[elementType] = 0;
            }

            var index = elementIndex[elementType];
            if(index < cache.Count) {
                var existing = cache[index];
                existing.SetActive(true);
                existing.transform.SetAsLastSibling();
                elementIndex[elementType] = index + 1;
                return existing;
            }

            var prefab = settings.GetPrefab(elementType);
            if(prefab == null) {
                Debug.LogWarning($"[PopupView] No prefab registered for element '{elementType}'");
                return null;
            }

            var created = Object.Instantiate(prefab, reference.Content);
            created.SetActive(true);
            created.transform.SetAsLastSibling();
            cache.Add(created);
            elementIndex[elementType] = index + 1;
            return created;
        }

        /// <summary>
        /// Deactivates all cached content elements and marks layout as dirty.
        /// </summary>
        public void ClearContent() {
            foreach(var kvp in elementCache) {
                var cache = kvp.Value;
                var activeCount = elementIndex[kvp.Key];
                for(int i = 0; i < activeCount; i++) {
                    cache[i].SetActive(false);
                }
                elementIndex[kvp.Key] = 0;
            }
            layoutDirty = true;
        }

        // --- Visibility ---

        /// <summary>Shows or hides the popup GameObject. Resets alpha to 0 when hiding.</summary>
        public void SetVisible(bool visible) {
            isVisible = visible;
            reference.gameObject.SetActive(visible);
            if(!visible) {
                reference.CanvasGroup.alpha = 0f;
            }
        }

        /// <summary>Constrains the popup's horizontal size to the given maximum width in pixels.</summary>
        public void SetMaxWidth(float maxPopupWidth) {
            maxWidth = maxPopupWidth;
            if(maxWidth > 0f) {
                var rt = (RectTransform)reference.transform;
                var size = rt.sizeDelta;
                size.x = Mathf.Min(size.x, maxWidth);
                rt.sizeDelta = size;
            }
        }

        // --- Positioning ---

        /// <summary>Configures the popup to anchor to a target element on the specified side.</summary>
        public void SetAnchorMode(RectTransform target, AnchorSide side, float edgePadding) {
            positionMode = PopupPositionMode.AnchorToElement;
            anchorTarget = target;
            anchorSide = side;
            screenEdgePadding = edgePadding;
        }

        /// <summary>Configures the popup to follow the mouse cursor with the given offset.</summary>
        public void SetFollowMouseMode(Vector2 offset, float edgePadding) {
            positionMode = PopupPositionMode.FollowMouse;
            followOffset = offset;
            screenEdgePadding = edgePadding;
        }

        /// <summary>Positions the popup at a fixed screen coordinate with edge clamping.</summary>
        public void SetFixedPosition(Vector2 screenPos, float edgePadding) {
            positionMode = PopupPositionMode.AnchorToElement;
            anchorTarget = null;
            screenEdgePadding = edgePadding;
            PositionAtScreenPoint(screenPos);
        }

        /// <summary>Updates the popup position based on the current mode (follow mouse or anchored).</summary>
        public void UpdatePosition() {
            if(positionMode == PopupPositionMode.FollowMouse) {
                PositionAtScreenPoint(Mouse.current.position.ReadValue() + followOffset);
            }
            else if(anchorTarget != null) {
                PositionAnchoredTo(anchorTarget, anchorSide);
            }
        }

        private void CacheCanvas() {
            if(rootCanvas != null) {
                return;
            }
            rootCanvas = reference.GetComponentInParent<Canvas>()?.rootCanvas;
            canvasCamera = rootCanvas != null && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay
                ? rootCanvas.worldCamera : null;
        }

        private void PositionAnchoredTo(RectTransform target, AnchorSide side) {
            CacheCanvas();
            var targetRect = GetScreenRect(target, canvasCamera);
            var popupRect = GetScreenRect((RectTransform)reference.transform, canvasCamera);
            var popupSize = popupRect.size;

            var pos = side switch {
                AnchorSide.Below => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMin - popupSize.y),
                AnchorSide.Above => new Vector2(targetRect.center.x - popupSize.x * 0.5f, targetRect.yMax),
                AnchorSide.Left => new Vector2(targetRect.xMin - popupSize.x, targetRect.center.y - popupSize.y * 0.5f),
                AnchorSide.Right => new Vector2(targetRect.xMax, targetRect.center.y - popupSize.y * 0.5f),
                _ => targetRect.center
            };

            PositionAtScreenPoint(pos);
        }

        private void PositionAtScreenPoint(Vector2 screenPos) {
            CacheCanvas();
            var rt = (RectTransform)reference.transform;
            if(layoutDirty) {
                LayoutRebuilder.ForceRebuildLayoutImmediate(reference.Content);
                layoutDirty = false;
            }
            var popupSize = GetScreenRect(rt, canvasCamera).size;

            // Clamp to screen
            screenPos.x = Mathf.Clamp(screenPos.x, screenEdgePadding, Screen.width - popupSize.x - screenEdgePadding);
            screenPos.y = Mathf.Clamp(screenPos.y, screenEdgePadding, Screen.height - popupSize.y - screenEdgePadding);

            // Convert screen position to parent local space
            var parentRt = rt.parent as RectTransform;
            if(parentRt != null) {
                RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRt, screenPos, canvasCamera, out var localPos);
                rt.localPosition = localPos;
            }
            else {
                rt.position = screenPos;
            }
        }

        private static readonly Vector3[] cornersBuffer = new Vector3[4];

        private static Rect GetScreenRect(RectTransform rt, Camera camera) {
            rt.GetWorldCorners(cornersBuffer);
            var min = RectTransformUtility.WorldToScreenPoint(camera, cornersBuffer[0]);
            var max = RectTransformUtility.WorldToScreenPoint(camera, cornersBuffer[2]);
            return new Rect(min, max - min);
        }
    }
}
