using System;
using System.Collections.Generic;
using Minesweeper.Config;
using Minesweeper.Core;
using Minesweeper.Presentation.Factories;
using UnityEngine;
using UnityEngine.UI;

namespace Minesweeper.Presentation.Views
{
    public sealed class BoardView : MonoBehaviour, IBoardView
    {
        private const float ResizeRefreshDelaySeconds = 0.5f;
        private const float ResizeSizeEpsilon = 0.5f;
        private const float MaximumCellPixelsPerUnitMultiplier = 1f;
        private const float ContentPaddingReferenceCellSize = 202f;
        private const float ContentPaddingReferencePadding = 15f;
        private const float MinimumContentPadding = 1f;

        [SerializeField] private GameObject root;
        [SerializeField] private RectTransform boardPanel;
        [SerializeField] private GridLayoutGroup gridLayoutGroup;
        [SerializeField] private MinesweeperUiConfig uiConfig;

        private readonly Dictionary<int, CellView> cellsByCoordinate = new Dictionary<int, CellView>();
        private readonly Stack<CellView> pooledCells = new Stack<CellView>();
        private IReadOnlyList<BoardCellData> currentCells;
        private readonly List<BoardCellData> currentCellsCache = new List<BoardCellData>();
        private bool inputEnabled = true;
        private bool currentRevealUnflaggedMines;
        private bool resizeRefreshPending;
        private int currentBoardWidth;
        private int currentBoardHeight;
        private float currentContentPadding = MinimumContentPadding;
        private float currentPixelsPerUnitMultiplier = 1f;
        private float resizeStableAt;
        private Vector2 lastObservedLayoutSize;

        private GameObject Root => root != null ? root : gameObject;

        public event Action CellPressStarted;
        public event Action CellPressEnded;
        public event Action PauseRequested;
        public event Action<int, int> CellOpenRequested;
        public event Action<int, int> CellFlagRequested;

        private void Awake()
        {
            if (root == null)
            {
                root = gameObject;
            }

            if (boardPanel == null)
            {
                boardPanel = transform as RectTransform;
            }
        }

        private void Update()
        {
            TrackResize();
        }

        public void BindConfig(MinesweeperUiConfig config)
        {
            uiConfig = config;
        }

        public void Show()
        {
            Root.SetActive(true);
            ResetResizeTracking();
        }

        public void Hide()
        {
            Root.SetActive(false);
        }

        public void Rebuild(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory, bool revealUnflaggedMines)
        {
            Clear();
            currentBoardWidth = width;
            currentBoardHeight = height;
            CacheCurrentCells(cells);
            currentRevealUnflaggedMines = revealUnflaggedMines;
            var layoutWasEnabled = gridLayoutGroup != null && gridLayoutGroup.enabled;
            SetGridLayoutEnabled(false);

            try
            {
                ConfigureGrid(width, height);

                for (var i = 0; i < cells.Count; i++)
                {
                    var cell = cells[i];
                    var view = CreateCell(cell, cellViewFactory);
                    if (view != null)
                    {
                        view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, currentContentPadding, revealUnflaggedMines);
                    }
                }
            }
            finally
            {
                SetGridLayoutEnabled(layoutWasEnabled);
            }
        }

        public void Refresh(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines)
        {
            CacheCurrentCells(cells);
            currentRevealUnflaggedMines = revealUnflaggedMines;

            RefreshCells(cells, revealUnflaggedMines);
        }

        public void RefreshCells(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines)
        {
            if (cells == null)
            {
                return;
            }

            for (var i = 0; i < cells.Count; i++)
            {
                var cell = cells[i];
                UpdateCachedCell(cell);
                if (cellsByCoordinate.TryGetValue(ToKey(cell.X, cell.Y), out var view))
                {
                    view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, currentContentPadding, revealUnflaggedMines);
                }
            }
        }

        public void SetInputEnabled(bool enabled)
        {
            inputEnabled = enabled;
            foreach (var cell in cellsByCoordinate.Values)
            {
                cell.SetInputEnabled(enabled);
            }
        }

        private void TrackResize()
        {
            if (boardPanel == null || !boardPanel.gameObject.activeInHierarchy || currentCells == null || currentBoardWidth <= 0 || currentBoardHeight <= 0)
            {
                return;
            }

            var layoutSize = GetLayoutSourceSize();
            if (layoutSize.x <= 0f || layoutSize.y <= 0f)
            {
                return;
            }

            if (HasSizeChanged(layoutSize, lastObservedLayoutSize))
            {
                lastObservedLayoutSize = layoutSize;
                resizeStableAt = Time.unscaledTime + ResizeRefreshDelaySeconds;
                resizeRefreshPending = true;
                return;
            }

            if (resizeRefreshPending && Time.unscaledTime >= resizeStableAt)
            {
                resizeRefreshPending = false;
                RefreshLayout();
            }
        }

        private void RefreshLayout()
        {
            ConfigureGrid(currentBoardWidth, currentBoardHeight);
            Refresh(currentCells, currentRevealUnflaggedMines);
        }

        private void ConfigureGrid(int width, int height)
        {
            if (gridLayoutGroup == null || boardPanel == null || uiConfig == null)
            {
                return;
            }

            Canvas.ForceUpdateCanvases();

            var layoutSize = GetLayoutSourceSize();
            var panelWidth = layoutSize.x > 0f ? layoutSize.x : uiConfig.ReferenceCellSize * width;
            var panelHeight = layoutSize.y > 0f ? layoutSize.y : uiConfig.ReferenceCellSize * height;
            var cellSize = CalculateCellSize(panelWidth, panelHeight, width, height);
            var padding = cellSize * uiConfig.BoardPaddingRatio;
            var spacing = cellSize * uiConfig.GridSpacingRatio;

            boardPanel.anchorMin = Vector2.zero;
            boardPanel.anchorMax = Vector2.one;
            boardPanel.offsetMin = new Vector2(padding, padding);
            boardPanel.offsetMax = new Vector2(-padding, -padding);

            gridLayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
            gridLayoutGroup.constraintCount = width;
            gridLayoutGroup.padding = new RectOffset();
            gridLayoutGroup.spacing = new Vector2(spacing, spacing);
            gridLayoutGroup.cellSize = new Vector2(cellSize, cellSize);
            currentPixelsPerUnitMultiplier = Mathf.Min(MaximumCellPixelsPerUnitMultiplier, uiConfig.ReferenceCellSize / cellSize);
            currentContentPadding = CalculateContentPadding(cellSize);
            lastObservedLayoutSize = layoutSize;
        }

        private Vector2 GetLayoutSourceSize()
        {
            var parentRect = boardPanel.parent as RectTransform;
            var rect = parentRect != null ? parentRect.rect : boardPanel.rect;
            return rect.size;
        }

        private void ResetResizeTracking()
        {
            resizeRefreshPending = false;
            if (boardPanel != null)
            {
                lastObservedLayoutSize = GetLayoutSourceSize();
            }
        }

        private float CalculateCellSize(float panelWidth, float panelHeight, int width, int height)
        {
            var widthUnits = width + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, width - 1) * uiConfig.GridSpacingRatio;
            var heightUnits = height + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, height - 1) * uiConfig.GridSpacingRatio;
            var cellByWidth = panelWidth / widthUnits;
            var cellByHeight = panelHeight / heightUnits;
            return Mathf.Max(uiConfig.MinimumCellSize, Mathf.Floor(Mathf.Min(cellByWidth, cellByHeight)));
        }

        private CellView CreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
        {
            if (gridLayoutGroup == null)
            {
                return null;
            }

            var view = GetOrCreateCell(cell, cellViewFactory);
            if (view == null)
            {
                return null;
            }

            view.SetInputEnabled(inputEnabled);
            view.OpenRequested += OnCellOpenRequested;
            view.FlagRequested += OnCellFlagRequested;
            view.PressStarted += OnCellPressStarted;
            view.PressEnded += OnCellPressEnded;
            cellsByCoordinate[ToKey(cell.X, cell.Y)] = view;
            return view;
        }

        private CellView GetOrCreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
        {
            CellView view;
            if (pooledCells.Count > 0)
            {
                view = pooledCells.Pop();
                view.transform.SetParent(gridLayoutGroup.transform, false);
                view.transform.SetAsLastSibling();
                view.Initialize(cell.X, cell.Y);
                view.gameObject.SetActive(true);
                return view;
            }

            view = cellViewFactory.CreateCell(cell, gridLayoutGroup.transform);
            if (view != null)
            {
                view.transform.SetAsLastSibling();
            }

            return view;
        }

        private void Clear()
        {
            foreach (var cell in cellsByCoordinate.Values)
            {
                if (cell != null)
                {
                    cell.OpenRequested -= OnCellOpenRequested;
                    cell.FlagRequested -= OnCellFlagRequested;
                    cell.PressStarted -= OnCellPressStarted;
                    cell.PressEnded -= OnCellPressEnded;
                    PoolCell(cell);
                }
            }

            cellsByCoordinate.Clear();
            currentCellsCache.Clear();
            currentCells = null;
        }

        private void CacheCurrentCells(IReadOnlyList<BoardCellData> cells)
        {
            currentCellsCache.Clear();
            if (cells != null)
            {
                for (var i = 0; i < cells.Count; i++)
                {
                    currentCellsCache.Add(cells[i]);
                }
            }

            currentCells = currentCellsCache;
        }

        private void UpdateCachedCell(BoardCellData cell)
        {
            var index = cell.Y * currentBoardWidth + cell.X;
            if (index >= 0 && index < currentCellsCache.Count)
            {
                currentCellsCache[index] = cell;
            }
        }

        private void PoolCell(CellView cell)
        {
            cell.gameObject.SetActive(false);
            pooledCells.Push(cell);
        }

        private void SetGridLayoutEnabled(bool enabled)
        {
            if (gridLayoutGroup != null)
            {
                gridLayoutGroup.enabled = enabled;
            }
        }

        private void OnCellOpenRequested(int x, int y)
        {
            CellOpenRequested?.Invoke(x, y);
        }

        private void OnCellFlagRequested(int x, int y)
        {
            CellFlagRequested?.Invoke(x, y);
        }

        private void OnCellPressStarted()
        {
            CellPressStarted?.Invoke();
        }

        private void OnCellPressEnded()
        {
            CellPressEnded?.Invoke();
        }

        private void OnPauseClicked()
        {
            PauseRequested?.Invoke();
        }

        private static float CalculateContentPadding(float cellSize)
        {
            return Mathf.Max(MinimumContentPadding, cellSize * ContentPaddingReferencePadding / ContentPaddingReferenceCellSize);
        }

        private static bool HasSizeChanged(Vector2 current, Vector2 previous)
        {
            return Mathf.Abs(current.x - previous.x) > ResizeSizeEpsilon || Mathf.Abs(current.y - previous.y) > ResizeSizeEpsilon;
        }

        private static int ToKey(int x, int y)
        {
            return (y << 16) ^ x;
        }
    }
}
