using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Enums;
using Models.Interfaces;
using Presenter;
using ScriptableObjects;
using Services.Interfaces;
using Structs;
using UnityEngine;
using Utils;
using VContainer.Unity;
using Views;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;

namespace Services {
    public class GameBoardService : IGameBoardService, ITickable, IDisposable {
        #region Inject
        private readonly IGameBoard gameBoard;
        private readonly GameVariables gameVariables;
        private readonly IMatchService matchService;
        private readonly IScoreService scoreService;
        private readonly IBombService bombService;
        private readonly IObjectPool<GemView> objectPool;
        private readonly Transform gemsHolder;
        #endregion
        
        #region Variables
        private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
        private readonly ScorePresenter scorePresenter;
        private GameState currentState = GameState.Move;
        #endregion
        
        public GameBoardService(IGameBoard gameBoard, GameVariables gameVariables, IMatchService matchService,  IScoreService scoreSerivce, IBombService bombService, IObjectPool<GemView> objectPool, Transform gemsHolder, ScorePresenter scorePresenter) {
            this.gameBoard = gameBoard;
            this.gameVariables = gameVariables;
            this.matchService = matchService;
            this.scoreService = scoreSerivce;
            this.bombService = bombService;
            this.objectPool = objectPool;
            this.gemsHolder = gemsHolder;
            this.scorePresenter = scorePresenter;
        }
        
        public void Tick() {
            foreach (GemPresenter gemPresenter in gemPresenters) {
                gemPresenter.Tick(this.gameVariables.gemSpeed);
            }

            this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
        }
        
        //Instantiates background tiles and calls SpawnGems
        //Uses MatchService.MatchesAt to avoid matching Gems
        public void Setup() {
            for (int x = 0; x < this.gameBoard.Width; x++)
                for (int y = 0; y < this.gameBoard.Height; y++)
                {
                    Vector2 position = new Vector2(x, y);
                    GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity);
                    backgroundTile.transform.SetParent(this.gemsHolder);
                    backgroundTile.name = "BG Tile - " + x + ", " + y;
                    
                    int gemToUse = RandomUtils.RandomGemTypeAsInt();

                    int iterations = 0;
                    while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100)
                    {
                        gemToUse = RandomUtils.RandomGemTypeAsInt();
                        iterations++;
                    }
                    
                    SpawnGem(new Vector2Int(x, y), (GemType)gemToUse);
                }
            
            this.currentState = GameState.Move;
        }

        //Uses the ObjectPool to spawn a gem at the given position
        private void SpawnGem(Vector2Int position, GemType gemType) {
            GemView gemView = this.objectPool.Get(gemType, position, this.gameVariables.dropHeight);
            gemView.name = "Gem - " + position.x + ", " + position.y + ' ' + gemType;

            // If we randomly spawned a bomb, give it a random color group (so it can match by color).
            int scoreValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs).scoreValue;
            Gem gem = new Gem(gemType, position, scoreValue);

            gemView.Bind(gem);

            this.gemPresenters.Add(new GemPresenter(gem, gemView));
            SetGem(new Vector2Int(position.x, position.y), gem);
        }
        
        private void SpawnBomb(Vector2Int position, GemType color) {
            // remove existing gem/view at that position
            DestroyMatchedGems(position);

            GemView gemView = this.objectPool.Get(GemType.Bomb, position, this.gameVariables.dropHeight);
            gemView.name = "Gem - " + position.x + ", " + position.y + ' ' + GemType.Bomb;

            int scoreValue = GemUtils.GetGemValues(color, this.gameVariables.gemsPrefabs).scoreValue;
            Gem bombGem = new Gem(GemType.Bomb, position, scoreValue, color);
            gemView.Bind(bombGem);

            this.gemPresenters.Add(new GemPresenter(bombGem, gemView));
            SetGem(position, bombGem);
        }

        //Sets the gem on the GameBoard
        private void SetGem(Vector2Int position, Gem gem) {
            this.gameBoard.SetGemAt(new Vector2Int(position.x, position.y), gem);
        }

        //Gets the gem from the GameBoard
        private Gem GetGem(Vector2Int position) {
            return this.gameBoard.GetGemAt(position);
        }
        
        //Listens to InputService OnSwapRequest
        public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
            if (this.currentState != GameState.Move) 
                return false;
            
            if (!InBounds(from) || !InBounds(to)) 
                return false;
            
            if (!AreAdjacentCardinal(from, to)) 
                return false;

            Gem fromGem = GetGem(from);
            Gem toGem = GetGem(to);
            
            if(fromGem == null || toGem == null) 
                return false;

            this.currentState = GameState.Wait;

            ApplySwap(from, to, fromGem, toGem);

            await UniTask.Delay(600);
            this.matchService.SetLastSwap(from, to);
            this.matchService.FindAllMatches();
            bool hasMatch = this.matchService.CurrentMatches.Count > 0;

            if (!hasMatch) {
                ApplySwap(to, from, fromGem, toGem);
                await UniTask.Delay(600);
                this.currentState = GameState.Move;
                return false;
            }
            
            List<Vector2Int> protectedPositions = ApplyPendingBombSpawns();

            await DestroyMatchesAsync(protectedPositions);
            this.currentState = GameState.Move;
            return true;
        }
        
        private void ApplySwap(Vector2Int posA, Vector2Int posB, Gem gemA, Gem gemB) {
            // swap their stored positions
            gemA.SetPosition(posB);
            gemB.SetPosition(posA);

            // update grid
            SetGem(posA, gemB);
            SetGem(posB, gemA);
        }
        
        private List<Vector2Int> ApplyPendingBombSpawns() {
            List<Vector2Int> positions = new List<Vector2Int>();

            foreach (BombSpawnRequest bomSpawnRequest in this.matchService.PendingBombSpawns) {
                positions.Add(bomSpawnRequest.Position);
                SpawnBomb(bomSpawnRequest.Position, bomSpawnRequest.Color);
            }

            this.matchService.ClearPendingBombs();
            return positions;
        }
        
        private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
            List<Vector2Int> matchPositions = new List<Vector2Int>(this.matchService.CurrentMatches.Count);
            for (int i = 0; i < this.matchService.CurrentMatches.Count; i++) {
                var m = this.matchService.CurrentMatches[i];
                if (m == null) continue;

                Vector2Int pos = m.Position;
                if (protectedPositions != null && protectedPositions.Contains(pos))
                    continue;

                matchPositions.Add(pos);
            }

            foreach (Vector2Int pos in matchPositions.Distinct().ToList()) {
                Gem gem = GetGem(pos);
                if (gem == null) continue;
                if (gem.Type == GemType.Bomb) continue;

                this.scoreService.ScoreCheck(gem.ScoreValue);
                DestroyMatchedGems(pos);
            }

            IReadOnlyList<Vector2Int> bombCandidates = this.bombService.CollectTriggeredBombs(matchPositions);

            List<Vector2Int> initialBombs = new List<Vector2Int>();
            foreach (Vector2Int p in bombCandidates) {
                if (!InBounds(p)) continue;

                if (protectedPositions != null && protectedPositions.Contains(p))
                    continue;

                Gem gem = GetGem(p);
                if (gem is { Type: GemType.Bomb })
                    initialBombs.Add(p);
            }
            initialBombs = initialBombs.Distinct().ToList();

            await this.bombService.DetonateChainAsync(initialBombs, InBounds, GetGem, DestroyAtAsync, this.gameVariables.bombRadius, this.gameVariables.bombDelay, this.gameVariables.bombSelfDelay);
            await MoveGemsDown();
        }

        private UniTask DestroyAtAsync(Vector2Int pos) {
            Gem gem = GetGem(pos);
            if (gem == null)
                return UniTask.CompletedTask;

            this.scoreService.ScoreCheck(gem.ScoreValue);
            DestroyMatchedGems(pos);
            return UniTask.CompletedTask;
        }

        private async UniTask MoveGemsDown() {
            await UniTask.Delay(50);

            int nullCounter = 0;
            for (int x = 0; x < this.gameBoard.Width; x++)
            {
                for (int y = 0; y < this.gameBoard.Height; y++)
                {
                    Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x, y));
                    if (currentGem == null)
                    {
                        nullCounter++;
                    }
                    else if (nullCounter > 0)
                    {
                        currentGem.SetPosition(new Vector2Int(currentGem.Position.x, currentGem.Position.y - nullCounter));
                        SetGem(currentGem.Position, currentGem);
                        SetGem(new Vector2Int(x,y), null);
                    }
                }
                nullCounter = 0;
            }

            await FillBoard();
        }

        private async UniTask FillBoard() {
            await UniTask.Delay(5);
            RefillBoard();
            await UniTask.Delay(5);

            this.matchService.FindAllMatches();
            if (this.matchService.CurrentMatches.Count > 0) {
                await UniTask.Delay(5);

                // In cascades, there is no "creating slot" bomb protection.
                await DestroyMatchesAsync(new List<Vector2Int>());
            } else {
                await UniTask.Delay(5);
                this.currentState = GameState.Move;
            }
        }

        private void RefillBoard() {
            for (int x = 0; x < this.gameBoard.Width; x++)
            {
                for (int y = 0; y < this.gameBoard.Height; y++)
                {
                    Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y));
                    if (currentGem == null) {
                        int gemToUse = RandomUtils.RandomGemTypeAsInt();
                        SpawnGem(new Vector2Int(x, y), (GemType)gemToUse);
                    }
                }
            }
        }

        private void DestroyMatchedGems(Vector2Int position) {
            List<GemView> gemsViews = GemsViews();
            Gem currentGem = this.gameBoard.GetGemAt(position);
            if (currentGem != null)
            {
                GemView gemView = gemsViews.FirstOrDefault(gv => gv.Gem == currentGem);
                if (gemView is null) {
                    return;
                }
                
                this.objectPool.Release(gemView);
                RemovePresenterFor(gemView);
                SetGem(position, null);
            }
        }
        
        #region Utils
        private void RemovePresenterFor(GemView gemView) {
            if (gemView is null) {
                return;
            }

            GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView);
            this.gemPresenters.Remove(presenter);
        }
        
        private List<GemView> GemsViews() {
            return this.gemsHolder.GetComponentsInChildren<GemView>().ToList();
        }
        
        private bool InBounds(Vector2Int p) {
            return p.x >= 0 && p.x < this.gameBoard.Width && p.y >= 0 && p.y < this.gameBoard.Height;
        }
        
        private static bool AreAdjacentCardinal(Vector2Int a, Vector2Int b) {
            Vector2Int d = b - a;
            return (Mathf.Abs(d.x) == 1 && d.y == 0) || (Mathf.Abs(d.y) == 1 && d.x == 0);
        }
        #endregion

        public void Dispose() {
            this.objectPool.Clear();
        }
    }
}