using System;
using System.Collections.Generic;
using Minesweeper.Config;

namespace Minesweeper.Core
{
    public sealed class BoardService : IBoardService
    {
        private const int DefaultWidth = 9;
        private const int DefaultHeight = 9;
        private const int DefaultMinesCount = 10;

        private readonly MinesweeperGameConfig config;
        private readonly Random random = new Random();
        private CellData[,] cells;

        public BoardService(MinesweeperGameConfig config)
        {
            this.config = config;
        }

        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 void InitializeEmptyBoard()
        {
            ResolveConfig(out var width, out var height, out var minesCount);

            Width = width;
            Height = height;
            MinesCount = minesCount;
            IsGenerated = false;
            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();

            cells[safeX, safeY].IsOpened = true;
            IsGenerated = true;
            return true;
        }

        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 ResolveConfig(out int width, out int height, out int minesCount)
        {
            width = config.Width;
            height = config.Height;
            minesCount = config.MinesCount;

            if (width <= 0 || height <= 0 || minesCount <= 0 || minesCount >= width * height)
            {
                width = DefaultWidth;
                height = DefaultHeight;
                minesCount = DefaultMinesCount;
            }
        }

        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 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; }
        }
    }
}
