using System.Collections.Generic;
using Minesweeper.Core;
using Minesweeper.ECS.Components;
using Unity.Collections;
using Unity.Entities;

namespace Minesweeper.ECS
{
    public sealed class BoardEcsSyncService : IBoardEcsSyncService
    {
        private readonly Dictionary<int, Entity> cellsByIndex = new Dictionary<int, Entity>();
        private int syncedWidth;
        private int syncedHeight;

        public void ClearBoard()
        {
            if (!TryGetEntityManager(out var entityManager))
            {
                return;
            }

            ClearCells(entityManager);
            cellsByIndex.Clear();
            syncedWidth = 0;
            syncedHeight = 0;
        }

        public void SyncBoard(IBoardService boardService)
        {
            if (!TryGetEntityManager(out var entityManager))
            {
                return;
            }

            if (syncedWidth != boardService.Width || syncedHeight != boardService.Height || cellsByIndex.Count != boardService.Width * boardService.Height)
            {
                ClearCells(entityManager);
                cellsByIndex.Clear();
                syncedWidth = boardService.Width;
                syncedHeight = boardService.Height;
            }
            else
            {
                ClearChangedTags(entityManager);
            }

            var boardEntity = GetOrCreateSingleton<BoardConfigComponent>(entityManager);
            entityManager.SetComponentData(boardEntity, new BoardConfigComponent
            {
                Width = boardService.Width,
                Height = boardService.Height,
                MinesCount = boardService.MinesCount,
                OpenedSafeCellsCount = boardService.OpenedSafeCellsCount,
                FlaggedCellsCount = boardService.FlaggedCellsCount,
                IsGenerated = ToByte(boardService.IsGenerated)
            });

            var archetype = entityManager.CreateArchetype(typeof(CellComponent));
            var cells = boardService.GetCells();
            for (var i = 0; i < cells.Count; i++)
            {
                var cell = cells[i];
                var index = ToIndex(cell.X, cell.Y, boardService.Width);
                if (!cellsByIndex.TryGetValue(index, out var entity) || !entityManager.Exists(entity))
                {
                    entity = entityManager.CreateEntity(archetype);
                    cellsByIndex[index] = entity;
                }

                SetCell(entityManager, entity, cell, boardService.Width, false);
            }
        }

        public void SyncCells(IReadOnlyList<BoardCellData> cells, IBoardService boardService)
        {
            if (cells == null || cells.Count == 0 || !TryGetEntityManager(out var entityManager))
            {
                return;
            }

            var boardEntity = GetOrCreateSingleton<BoardConfigComponent>(entityManager);
            entityManager.SetComponentData(boardEntity, new BoardConfigComponent
            {
                Width = boardService.Width,
                Height = boardService.Height,
                MinesCount = boardService.MinesCount,
                OpenedSafeCellsCount = boardService.OpenedSafeCellsCount,
                FlaggedCellsCount = boardService.FlaggedCellsCount,
                IsGenerated = ToByte(boardService.IsGenerated)
            });

            ClearChangedTags(entityManager);

            for (var i = 0; i < cells.Count; i++)
            {
                var cell = cells[i];
                var index = ToIndex(cell.X, cell.Y, boardService.Width);
                if (cellsByIndex.TryGetValue(index, out var entity) && entityManager.Exists(entity))
                {
                    SetCell(entityManager, entity, cell, boardService.Width, true);
                }
            }
        }

        public void SyncGameState(GameState state, bool hasFirstClick)
        {
            if (!TryGetEntityManager(out var entityManager))
            {
                return;
            }

            var stateEntity = GetOrCreateSingleton<GameStateComponent>(entityManager);
            entityManager.SetComponentData(stateEntity, new GameStateComponent
            {
                State = state,
                HasFirstClick = ToByte(hasFirstClick)
            });
        }

        private static bool TryGetEntityManager(out EntityManager entityManager)
        {
            var world = World.DefaultGameObjectInjectionWorld;
            if (world == null || !world.IsCreated)
            {
                entityManager = default;
                return false;
            }

            entityManager = world.EntityManager;
            return true;
        }

        private static void ClearCells(EntityManager entityManager)
        {
            var query = entityManager.CreateEntityQuery(typeof(CellComponent));
            entityManager.DestroyEntity(query);
            query.Dispose();
        }

        private static void ClearChangedTags(EntityManager entityManager)
        {
            var query = entityManager.CreateEntityQuery(typeof(CellChangedTag));
            entityManager.RemoveComponent<CellChangedTag>(query);
            query.Dispose();
        }

        private static void SetCell(EntityManager entityManager, Entity entity, BoardCellData cell, int width, bool markChanged)
        {
            entityManager.SetComponentData(entity, new CellComponent
            {
                X = cell.X,
                Y = cell.Y,
                Index = ToIndex(cell.X, cell.Y, width),
                IsMine = ToByte(cell.IsMine),
                IsOpened = ToByte(cell.IsOpened),
                IsFlagged = ToByte(cell.IsFlagged),
                NeighborMines = cell.NeighborMines
            });

            if (markChanged && !entityManager.HasComponent<CellChangedTag>(entity))
            {
                entityManager.AddComponent<CellChangedTag>(entity);
            }
        }

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

        private static Entity GetOrCreateSingleton<T>(EntityManager entityManager) where T : unmanaged, IComponentData
        {
            var query = entityManager.CreateEntityQuery(typeof(T));
            Entity entity;

            if (query.IsEmptyIgnoreFilter)
            {
                entity = entityManager.CreateEntity(typeof(T));
            }
            else
            {
                var entities = query.ToEntityArray(Allocator.Temp);
                entity = entities[0];
                for (var i = 1; i < entities.Length; i++)
                {
                    entityManager.DestroyEntity(entities[i]);
                }

                entities.Dispose();
            }

            query.Dispose();
            return entity;
        }

        private static byte ToByte(bool value)
        {
            return value ? (byte)1 : (byte)0;
        }
    }
}
