using System;
using System.Collections.Generic;

namespace Minesweeper.Core
{
    public sealed class BoardService : IBoardService
    {
        private readonly IGameSettingsService settingsService;
        private readonly Random random = new Random();
        private readonly List<BoardCellData> changedCells = new List<BoardCellData>();
        private CellData[,] cells;

        public BoardService(IGameSettingsService settingsService)
        {
            this.settingsService = settingsService;
        }

        public int Width { get; private set; }
        public int Height { get; private set; }
        public int MinesCount { get; private set; }
        public bool IsGenerated { get; private set; }
        public int OpenedSafeCellsCount { get; private set; }
        public int FlaggedCellsCount { get; private set; }
        public int SafeCellsCount => Width * Height - MinesCount;
        public IReadOnlyList<BoardCellData> LastChangedCells => changedCells;

        public void InitializeEmptyBoard()
        {
            Width = settingsService.SizeX;
            Height = settingsService.SizeY;
            MinesCount = Math.Min(settingsService.MinesCount, Width * Height - 1);
            OpenedSafeCellsCount = 0;
            FlaggedCellsCount = 0;
            IsGenerated = false;
            changedCells.Clear();
            cells = new CellData[Width, Height];

            for (var x = 0; x < Width; x++)
            {
                for (var y = 0; y < Height; y++)
                {
                    cells[x, y] = new CellData(x, y);
                }
            }
        }

        public bool GenerateAfterFirstClick(int safeX, int safeY)
        {
            EnsureInitialized();

            if (IsGenerated || !IsInside(safeX, safeY))
            {
                return false;
            }

            PlaceMines(safeX, safeY);
            CalculateNeighborMines();

            IsGenerated = true;
            return true;
        }

        public BoardActionResult OpenCell(int x, int y)
        {
            EnsureInitialized();
            changedCells.Clear();

            if (!IsGenerated || !IsInside(x, y))
            {
                return BoardActionResult.InvalidAction;
            }

            var cell = cells[x, y];
            if (cell.IsOpened || cell.IsFlagged)
            {
                return BoardActionResult.NoChange;
            }

            if (cell.IsMine)
            {
                cell.IsOpened = true;
                AddChangedCell(cell);
                return new BoardActionResult(true, true, false, false, changedCells);
            }

            if (cell.NeighborMines == 0)
            {
                RevealEmptyArea(x, y);
            }
            else
            {
                OpenSafeCell(cell);
            }

            return new BoardActionResult(true, false, IsWin(), false, changedCells);
        }

        public BoardActionResult ToggleFlag(int x, int y)
        {
            EnsureInitialized();
            changedCells.Clear();

            if (!IsInside(x, y))
            {
                return BoardActionResult.InvalidAction;
            }

            var cell = cells[x, y];
            if (cell.IsOpened)
            {
                return BoardActionResult.NoChange;
            }

            if (cell.IsFlagged)
            {
                cell.IsFlagged = false;
                FlaggedCellsCount--;
            }
            else
            {
                cell.IsFlagged = true;
                FlaggedCellsCount++;
            }

            AddChangedCell(cell);
            return new BoardActionResult(true, false, false, false, changedCells);
        }

        public bool IsInside(int x, int y)
        {
            return cells != null && x >= 0 && y >= 0 && x < Width && y < Height;
        }

        public bool TryGetCell(int x, int y, out BoardCellData cell)
        {
            if (!IsInside(x, y))
            {
                cell = default;
                return false;
            }

            cell = ToData(cells[x, y]);
            return true;
        }

        public IReadOnlyList<BoardCellData> GetCells()
        {
            EnsureInitialized();

            var result = new List<BoardCellData>(Width * Height);
            for (var y = 0; y < Height; y++)
            {
                for (var x = 0; x < Width; x++)
                {
                    result.Add(ToData(cells[x, y]));
                }
            }

            return result;
        }

        private void EnsureInitialized()
        {
            if (cells == null)
            {
                InitializeEmptyBoard();
            }
        }

        private void PlaceMines(int safeX, int safeY)
        {
            var positions = new List<int>(Width * Height - 1);
            for (var x = 0; x < Width; x++)
            {
                for (var y = 0; y < Height; y++)
                {
                    if (x == safeX && y == safeY)
                    {
                        continue;
                    }

                    positions.Add(ToIndex(x, y));
                }
            }

            Shuffle(positions);

            for (var i = 0; i < MinesCount; i++)
            {
                var index = positions[i];
                var x = index % Width;
                var y = index / Width;
                cells[x, y].IsMine = true;
            }
        }

        private void Shuffle(List<int> values)
        {
            for (var i = values.Count - 1; i > 0; i--)
            {
                var j = random.Next(i + 1);
                (values[i], values[j]) = (values[j], values[i]);
            }
        }

        private void CalculateNeighborMines()
        {
            for (var x = 0; x < Width; x++)
            {
                for (var y = 0; y < Height; y++)
                {
                    if (cells[x, y].IsMine)
                    {
                        cells[x, y].NeighborMines = 0;
                        continue;
                    }

                    cells[x, y].NeighborMines = CountNeighborMines(x, y);
                }
            }
        }

        private int CountNeighborMines(int centerX, int centerY)
        {
            var count = 0;
            for (var x = centerX - 1; x <= centerX + 1; x++)
            {
                for (var y = centerY - 1; y <= centerY + 1; y++)
                {
                    if (x == centerX && y == centerY)
                    {
                        continue;
                    }

                    if (IsInside(x, y) && cells[x, y].IsMine)
                    {
                        count++;
                    }
                }
            }

            return count;
        }

        private void RevealEmptyArea(int startX, int startY)
        {
            var visited = new bool[Width, Height];
            var queue = new Queue<CellData>();
            queue.Enqueue(cells[startX, startY]);

            while (queue.Count > 0)
            {
                var cell = queue.Dequeue();
                if (visited[cell.X, cell.Y] || cell.IsMine)
                {
                    continue;
                }

                visited[cell.X, cell.Y] = true;
                ClearFlag(cell);
                OpenSafeCell(cell);

                if (cell.NeighborMines != 0)
                {
                    continue;
                }

                for (var x = cell.X - 1; x <= cell.X + 1; x++)
                {
                    for (var y = cell.Y - 1; y <= cell.Y + 1; y++)
                    {
                        if ((x == cell.X && y == cell.Y) || !IsInside(x, y))
                        {
                            continue;
                        }

                        var neighbor = cells[x, y];
                        if (!visited[x, y] && !neighbor.IsMine && !neighbor.IsOpened)
                        {
                            queue.Enqueue(neighbor);
                        }
                    }
                }
            }
        }

        private void OpenSafeCell(CellData cell)
        {
            if (cell.IsOpened || cell.IsMine)
            {
                return;
            }

            cell.IsOpened = true;
            OpenedSafeCellsCount++;
            AddChangedCell(cell);
        }

        private void ClearFlag(CellData cell)
        {
            if (!cell.IsFlagged)
            {
                return;
            }

            cell.IsFlagged = false;
            FlaggedCellsCount--;
        }

        private void AddChangedCell(CellData cell)
        {
            changedCells.Add(ToData(cell));
        }

        private bool IsWin()
        {
            return OpenedSafeCellsCount >= SafeCellsCount;
        }

        private int ToIndex(int x, int y)
        {
            return y * Width + x;
        }

        private static BoardCellData ToData(CellData cell)
        {
            return new BoardCellData(cell.X, cell.Y, cell.IsMine, cell.IsOpened, cell.IsFlagged, cell.NeighborMines);
        }

        private sealed class CellData
        {
            public CellData(int x, int y)
            {
                X = x;
                Y = y;
            }

            public int X { get; }
            public int Y { get; }
            public bool IsMine { get; set; }
            public bool IsOpened { get; set; }
            public bool IsFlagged { get; set; }
            public int NeighborMines { get; set; }
        }
    }
}
