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;

namespace Services {
    public class GameBoardService : IGameBoardService, ITickable, IDisposable {
        #region Inject
        private readonly GameVariables gameVariables;
        
        private readonly IGameBoard gameBoard;
        private readonly IObjectPool<GemView> objectPool;
        
        private readonly IMatchService matchService;
        private readonly IBombService bombService;
        private readonly IScoreService scoreService;
        
        private readonly ScorePresenter scorePresenter;
        private readonly AudioPresenter audioPresenter;
        
        private readonly Transform backgroundHolder;
        #endregion
        
        #region Variables
        private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
        private readonly Dictionary<Gem, GemView> gemToView = new Dictionary<Gem, GemView>();
        private GameState currentState = GameState.Setup;
        #endregion
        
        public GameBoardService(
                GameVariables gameVariables, 
                IGameBoard gameBoard, 
                IObjectPool<GemView> objectPool, 
                IMatchService matchService,
                IBombService bombService, 
                IScoreService scoreService, 
                ScorePresenter scorePresenter, 
                AudioPresenter audioPresenter,
                Transform backgroundHolder) {
            this.gameVariables = gameVariables;
            this.gameBoard = gameBoard;
            this.objectPool = objectPool;
            this.matchService = matchService;
            this.bombService = bombService;
            this.scoreService = scoreService;
            this.scorePresenter = scorePresenter;
            this.audioPresenter = audioPresenter;
            this.backgroundHolder = backgroundHolder;
        }
        
        /// <summary>
        /// Global tick. Update replacement of VContainer-managed objects.
        /// </summary>
        public void Tick() {
            foreach (GemPresenter gemPresenter in gemPresenters) {
                gemPresenter.Tick(this.gameVariables.gemSpeed);
            }

            this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
        }
        
        /// <summary>
        /// Initiates the game board by placing gems randomly.
        /// </summary>
        public void Setup() {
            List<Gem> gemsToSpawn = new List<Gem>();
            for (int x = 0; x < this.gameBoard.Width; x++)
                for (int y = 0; y < this.gameBoard.Height; y++)
                {
                    Vector2 position = new Vector2(x, y);
                    SpawnBackgroundTile(position);

                    int iterations = 0;
                    GemType gemToUse;
                    do {
                        gemToUse = RandomUtils.RandomGemType();
                        iterations++;
                    } while (this.matchService.MatchesAt(position.ToVector2Int(), gemToUse) && iterations < 100);
                    
                    gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), gemToUse));
                }

            SpawnCascade(gemsToSpawn).Forget();
        }
        
        /// <summary>
        /// Create a Gem object and place it on the GameBoard.
        /// </summary>
        /// <param name="position">
        /// Position on the GameBoard to place the Gem.
        /// </param>
        /// <param name="gemType">
        /// Type of Gem to create.
        /// </param>
        /// <param name="isBomb">
        /// Whether the Gem is a Bomb or not.
        /// </param>
        /// <returns></returns>
        private Gem SetGemAt(Vector2Int position, GemType gemType, bool isBomb = false) {
            GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs);
            Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType);
            this.gameBoard.SetGemAt(position, gem);

            return gem;
        }

        /// <summary>
        /// Spawns a GemView object and binds it to the Gem object.
        /// </summary>
        /// <param name="gem">
        /// Gem to bind to the GemView.
        /// </param>
        /// <param name="isBomb">
        /// Whether the Gem is a Bomb or not.
        /// </param>
        private void SpawnGemGameObject(Gem gem, bool isBomb = false) {
            GemView gemView = this.objectPool.Get(isBomb ? GemType.Bomb : gem.Type, gem.Position, isBomb ? 0 : this.gameVariables.dropHeight);
            gemView.name = "Gem - " + gem.Position.x + ", " + gem.Position.y + ' ' + gem.Type;
            
            GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs);
            gemView.Bind(gem, gemValue, isBomb: isBomb);
            this.gemPresenters.Add(new GemPresenter(gem, gemView));
            this.gemToView.Add(gem, gemView);
        }

        /// <summary>
        /// Calls SetGemAt and SpawnGemGameObject.
        /// </summary>
        /// <param name="position">
        /// Position on the GameBoard to place the Gem.
        /// </param>
        /// <param name="gemType">
        /// Type of Gem to create.
        /// </param>
        /// <param name="isBomb">
        /// Whether the Gem is a Bomb or not.
        /// </param>
        private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) {
            if(isBomb)
                ReleaseMatchedGems(position);
            
            SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb);
        }

        /// <summary>
        /// Spawns a background tile at the given position. Only for setup.
        /// </summary>
        /// <param name="position">
        /// Position on the GameBoard to place the tile.
        /// </param>
        private void SpawnBackgroundTile(Vector2 position) {
            GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity);
            backgroundTile.transform.SetParent(this.backgroundHolder);
            backgroundTile.name = "BG Tile - " + position.x + ", " + position.y;
        }
        
        /// <summary>
        /// Attempts to swap two gems on the GameBoard.
        /// </summary>
        /// <param name="from">
        /// Original position of the gem to swap.
        /// </param>
        /// <param name="to">
        /// Destination position of the gem to swap.
        /// </param>
        /// <returns></returns>
        public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
            if (this.currentState != GameState.Move) 
                return false;
            
            if (!GemUtils.IsInBounds(from, this.gameBoard) || !GemUtils.IsInBounds(to, this.gameBoard)) 
                return false;

            if (!from.IsAdjacent(to)) 
                return false;
            
            this.currentState = GameState.Wait;

            ApplySwap(from, to);

            await UniTask.Delay(this.gameVariables.swapDelayMs);
            this.bombService.SetLastSwap(from, to);
            this.matchService.FindAllMatches();
            this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches);
            
            if (this.matchService.CurrentMatches.Count == 0) {
                ApplySwap(to, from);
                await UniTask.Delay(this.gameVariables.swapDelayMs);
                this.currentState = GameState.Move;
                return false;
            }
            
            List<Vector2Int> protectedPositions = this.bombService.ApplyPendingBombSpawns(SetAndSpawnGem);
            await DestroyMatchesAsync(protectedPositions);
            this.currentState = GameState.Move;
            return true;
        }

        /// <summary>
        /// For debug purposes. Switches the Gem at the given position to a different GemType.
        /// </summary>
        /// <param name="position">
        /// Position of the Gem to switch.
        /// </param>
        /// <returns></returns>
        public void TrySwitch(Vector2Int position) {
            if (!this.gameVariables.debugMode)
                return;
            
            Gem gem = this.gameBoard.GetGemAt(position);
            if(gem == null)
                return;
            
            GemType[] normalTypes = Enum.GetValues(typeof(GemType))
                .Cast<GemType>()
                .Where(t => t != GemType.Bomb)
                .ToArray();

            if (normalTypes.Length == 0)
                return;
            
            bool nextIsBomb;
            GemType nextTypeOrMatchColor;

            if (gem.Type != GemType.Bomb) {
                int index = Array.IndexOf(normalTypes, gem.Type);
                if (index < 0) index = 0;

                if (index < normalTypes.Length - 1) {
                    nextIsBomb = false;
                    nextTypeOrMatchColor = normalTypes[index + 1];
                } else {
                    nextIsBomb = true;
                    nextTypeOrMatchColor = normalTypes[0]; 
                }
            } else {
                int idx = Array.IndexOf(normalTypes, gem.MatchColor);
                if (idx < 0) idx = 0;

                if (idx < normalTypes.Length - 1) {
                    nextIsBomb = true;
                    nextTypeOrMatchColor = normalTypes[idx + 1];
                } else {
                    nextIsBomb = false;
                    nextTypeOrMatchColor = normalTypes[0];
                }
            }

            // Replace both model+view by releasing current and respawning at the same position.
            ReleaseMatchedGems(position);
            SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb);
        }
        
        /// <summary>
        /// Helper function for TrySwap. Swaps the Gems in the GameBoard.
        /// </summary>
        /// <param name="from">
        /// Original position of the gem to swap.
        /// </param>
        /// <param name="to">
        /// Destination position of the gem to swap.
        /// </param>
        private void ApplySwap(Vector2Int from, Vector2Int to) {
            Gem fromGem = this.gameBoard.GetGemAt(from);
            Gem toGem = this.gameBoard.GetGemAt(to);
            // swap their stored positions
            fromGem.SetPosition(to);
            toGem.SetPosition(from);

            // update grid
            this.gameBoard.SetGemAt(from, toGem);
            this.gameBoard.SetGemAt(to, fromGem);
        }
        
        /// <summary>
        /// Destroys all matches on the GameBoard.
        /// </summary>
        /// <param name="protectedPositions">
        /// Gems we don't want to instantly destroy.
        /// </param>
        private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
            HashSet<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions);
            List<Vector2Int> initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions);

            if (initialBombs.Count > 0) {
                await this.bombService.DetonateChainAsync(
                    initialBombs,
                    DestroyAtAsync);
                
                foreach (Vector2Int p in matchPositions)
                    await DestroyAtAsync(p);

                await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
                
                await MoveGemsDown();
                return;
            }

            // For audio SFX
            bool willBreakAnyNonBombGem = false;
            foreach (Vector2Int pos in matchPositions) {
                Gem g = this.gameBoard.GetGemAt(pos);
                if (g != null && g.Type != GemType.Bomb) {
                    willBreakAnyNonBombGem = true;
                    break;
                }
            }
            if (willBreakAnyNonBombGem)
                this.audioPresenter.OnMatch(this.gameVariables.matchSfx);

            // For score counting
            foreach (Vector2Int pos in matchPositions) {
                Gem gem = this.gameBoard.GetGemAt(pos);
                if (gem == null) continue;
                if (gem.Type == GemType.Bomb) continue;

                this.scoreService.AddScore(gem.ScoreValue);
                ReleaseMatchedGems(pos);
            }
            await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
            await MoveGemsDown();
        }

        /// <summary>
        /// Destroys the Gem at the given position.
        /// </summary>
        /// <param name="pos">
        /// Position of the Gem to destroy.
        /// </param>
        /// <returns></returns>
        private UniTask DestroyAtAsync(Vector2Int pos) {
            Gem gem = this.gameBoard.GetGemAt(pos);
            if (gem == null)
                return UniTask.CompletedTask;
            
            if (gem.Type == GemType.Bomb)
                this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx);

            this.scoreService.AddScore(gem.ScoreValue);
            ReleaseMatchedGems(pos);
            return UniTask.CompletedTask;
        }
        
        /// <summary>
        /// Returns the GemView to the Object Pool and sets the Position in the GameBoard to null.
        /// </summary>
        /// <param name="position">
        /// Position of the Gem to release.
        /// </param>
        private void ReleaseMatchedGems(Vector2Int position) {
            Gem currentGem = this.gameBoard.GetGemAt(position);
            if (currentGem != null)
            {
                if (!this.gemToView.TryGetValue(currentGem, out GemView gemView) || gemView == null)
                    return;
                
                this.objectPool.Release(gemView);
                RemovePresenterFor(gemView);
                this.gameBoard.SetGemAt(position, null);
            }
        }

        /// <summary>
        /// Moves Gems Down to free spaces.
        /// </summary>
        private async UniTask MoveGemsDown() {
            List<FallMove> moves = new List<FallMove>();
            while (true) {
                moves.Clear();
                for (int x = 0; x < this.gameBoard.Width; x++)
                {
                    for (int y = 1; y < this.gameBoard.Height; y++)
                    {
                        Vector2Int from = new Vector2Int(x, y);
                        Vector2Int to = new Vector2Int(x, y - 1);

                        Gem gem = this.gameBoard.GetGemAt(from);
                        if (gem == null)
                            continue;

                        if (this.gameBoard.GetGemAt(to) != null)
                            continue;

                        moves.Add(new FallMove {
                            from = from,
                            to = to,
                            gem = gem
                        });
                    }
                }

                if (moves.Count == 0)
                    break;
                
                foreach (FallMove move in moves) {
                    this.gameBoard.SetGemAt(move.to, move.gem);
                    this.gameBoard.SetGemAt(move.from, null);
                    move.gem.SetPosition(move.to);
                }

                await UniTask.Delay(this.gameVariables.cascadeDelayMs);
            }

            await FillBoard();
        }

        /// <summary>
        /// Fills the GameBoard with gems. If there are resulting matches, process them.
        /// </summary>
        private async UniTask FillBoard() {
            await RefillBoard();

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

                await DestroyMatchesAsync(new List<Vector2Int>());
            } else {
                this.currentState = GameState.Move;
            }
        }

        /// <summary>
        /// Refills the GameBoard with gems.
        /// </summary>
        private async UniTask RefillBoard() {
            List<Gem> gemsToSpawn = new List<Gem>();
            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) {
                        GemType gemToUse = RandomUtils.RandomGemType();

                        int iterations = 0;
                        while (this.matchService.MatchesAt(new Vector2Int(x, y), gemToUse) && iterations < 100)
                        {
                            gemToUse = RandomUtils.RandomGemType();
                            iterations++;
                        }
                        
                        gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), gemToUse));
                    }
                }
            }

            await SpawnCascade(gemsToSpawn);
        }

        /// <summary>
        /// Spawns gems in cascade.
        /// </summary>
        /// <param name="gemsToSpawn">
        /// List of gems to spawn.
        /// </param>
        private async UniTask SpawnCascade(List<Gem> gemsToSpawn)
        {
            List<IGrouping<int, Gem>> groups = gemsToSpawn
                .GroupBy(gem => gem.Position.y)
                .OrderBy(group => group.Key)
                .ToList();

            for (int i = 0; i < groups.Count; i++)
            {
                foreach (Gem gem in groups[i])
                    SpawnGemGameObject(gem);

                if (i < groups.Count - 1)
                    await UniTask.Delay(this.gameVariables.cascadeDelayMs);
            }
            
            if(this.currentState == GameState.Setup)
                this.currentState = GameState.Move;
        }
        
        /// <summary>
        /// Removes the Presenter for the given GemView from the list to avoid calling its Tick method.
        /// </summary>
        /// <param name="gemView">
        /// GemView to remove from the List of presenters.
        /// </param>
        private void RemovePresenterFor(GemView gemView) {
            if (gemView is null) {
                return;
            }

            GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView);
            this.gemPresenters.Remove(presenter);
        }

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