using System;
using UnityEngine;

namespace Marro.PacManUdon
{
    enum EatResult
    {
        None,
        Pellet,
        PowerPellet
    }

    public class CollisionManager : SyncedObject
    {
        public int PelletCount => pellets.Length;

        private GameManager gameManager;
        private BonusFruit bonusFruit;
        private Pellet[] pellets;
        private Animator[] powerPellets;
        private Ghost[] ghosts;

        private byte[] collisionMap;
        private int[] pelletIndices;

        private const int mazeWidth = 32;
        private const int mazeHeight = 32;

        private bool powerPelletBlinkEnabled;
        private float powerPelletBlinkToggleInterval;
        private float powerPelletBlinkProgress;
        private bool powerPelletBlinkCurrentlyVisible;

        private byte[] pelletMap;
        private byte[] syncedPelletsCollected;
        private byte pelletCollectedCount;

        private int[] ghostPositions = new int[4];
        private int pacManPosition;

        private bool frozen;

        public void Initialize(GameManager gameManager, BonusFruit bonusFruit, Ghost[] ghosts)
        {
            this.gameManager = gameManager;
            this.bonusFruit = bonusFruit;
            this.ghosts = ghosts;

            gameObject.SetActive(true);
            pellets = GetComponentsInChildren<Pellet>(includeInactive: true);

            powerPellets = GetComponentsInChildren<Animator>(true);
            powerPelletBlinkToggleInterval = PacManConstants.GetPowerPelletBlinkToggleInterval();

            collisionMap = PacManConstants.GetMazeCollisionInfo();
            pelletIndices = PacManConstants.GetMazePelletIndices();

            Reset();
        }

        public void Reset()
        {
            SetPowerPelletsBlink(false);

            RestoreAllPellets();
        }

        internal void SetFrozen(bool frozen)
        {
            this.frozen = frozen;

            if (!frozen)
            {
                SetPowerPelletsBlink(true);
            }
        }

        #region Collision
        public bool IsWallUpcoming(Vector2 position, Vector2 directionVector)
        {
            var index = GetTilemapIndex(position + directionVector);
            var tile = collisionMap[index];
            var result = (tile & (int)PacManCollisionInfoType.Wall) != 0;
            //Debug.Log($"IsWallUpcoming {position}, {directionVector}. index {index}, tile {tile}, result {result}, collisionMap.Length {collisionMap.Length}");
            return result;
        }

        public bool GhostMoveToTile(Vector2 position, int ghostIndex)
        {
            var tile = GetTilemapIndex(position);

            ghostPositions[ghostIndex] = tile;

            if (!frozen && tile == pacManPosition)
            {
                //Debug.Log("Ghost hit PacMan!");
                ghosts[ghostIndex].HitPacMan();
            }

            return (collisionMap[tile] & (int)PacManCollisionInfoType.Tunnel) != 0;
        }

        internal EatResult PacManMoveToTile(Vector2 position, Vector2 nextPosition)
        {
            var tilemapIndex = GetTilemapIndex(nextPosition);
            var tile = pelletMap[tilemapIndex];

            pacManPosition = tilemapIndex;

            if (!frozen)
            {
                PacManTryEatGhost();
            }

            TryCollectFruit(tile, position);
            return TryCollectPellet(tile, tilemapIndex);
        }

        internal void PacManTryEatGhost()
        {
            for (int i = 0; i < ghosts.Length; i++)
            {
                if (ghostPositions[i] != pacManPosition)
                {
                    continue;
                }

                if (ghosts[i].HitPacMan()) // Only one collision may happen at a time
                {
                    //Debug.Log("PacMan hit ghost!");
                    return;
                }
            }
        }

        private void TryCollectFruit(int tile, Vector2 position)
        {
            if (tile != (int)PacManConsumableType.FruitLeft && tile != (int)PacManConsumableType.FruitRight
                || !bonusFruit.Active)
            {
                return;
            }

            var previousTile = pelletMap[GetTilemapIndex(position)];

            //Debug.Log($"On fruit tile {tile}, previous tile was {previousTile}. Position {position}, nextPosition {nextPosition}");

            if (tile == (int)PacManConsumableType.FruitLeft && previousTile == (int)PacManConsumableType.FruitRight
                || tile == (int)PacManConsumableType.FruitRight && previousTile == (int)PacManConsumableType.FruitLeft)
            {
                //Debug.Log("Collecting fruit");

                gameManager.GotFruit();
            }
        }

        public int GetAvailableDirections(Vector2 position)
        {
            var directions = collisionMap[GetTilemapIndex(position)];

            return directions;
        }

        internal static int GetTilemapIndex(Vector2 position)
        {
            // (int)position.x %% mazeWidth + (int)position.y %% mazeHeight * mazeWidth, where %% is an absolute modulo
            // Absoliute modulo in this case is implemented by just adding the divisor, works good enough and is performant.
            var index = ((int)position.x + mazeWidth) % mazeWidth + ((int)position.y + mazeHeight) % mazeHeight * mazeWidth;

            return index;
        }
        #endregion

        #region Pellet collecting
        private EatResult TryCollectPellet(int tile, int tilemapIndex)
        {
            if (tile < 0 || tile >= pellets.Length)
            {
                return EatResult.None;
            }

            pelletMap[tilemapIndex] = (byte)PacManConsumableType.None;

            var pellet = pellets[tile];
            pellet.gameObject.SetActive(false);

            var index = pellet.transform.GetSiblingIndex();
            syncedPelletsCollected[index / 8] |= (byte)(1 << index % 8);

            pelletCollectedCount++;

            var pelletType = pellet.isPowerPellet ? EatResult.PowerPellet : EatResult.Pellet;

            gameManager.GotPellet(pellet, pellet.isPowerPellet, pelletCollectedCount, PelletCount - pelletCollectedCount);

            return pelletType;
        }

        public int RestoreAllPellets()
        {
            foreach (var pellet in pellets)
            {
                pellet.gameObject.SetActive(true);
            }

            syncedPelletsCollected = new byte[pellets.Length/8 + 1];
            pelletCollectedCount = 0;

            pelletMap = PacManConstants.GetMazePelletMap();

            return PelletCount;
        }

        private void SetPelletsCollectedFromSync()
        {
            for (byte i = 0; i < pellets.Length; i++)
            {
                var active = (syncedPelletsCollected[i/8] & (byte)(1 << i%8)) == 0;
                pellets[i].gameObject.SetActive(active);
                pelletMap[pelletIndices[i]] = active ? i : (byte)PacManConsumableType.None;
            }
        }
        #endregion

        #region Power pellet blink
        public override void SyncedUpdate()
        {
            if (!powerPelletBlinkEnabled)
            {
                return;
            }

            powerPelletBlinkProgress += networkManager.SyncedDeltaTime;
            if (powerPelletBlinkProgress >= powerPelletBlinkToggleInterval)
            {
                // Debug.Log($"{gameObject} PowerPelletBlink toggle");
                powerPelletBlinkProgress -= powerPelletBlinkToggleInterval;
                powerPelletBlinkCurrentlyVisible = !powerPelletBlinkCurrentlyVisible;
                SetPowerPelletsVisible(powerPelletBlinkCurrentlyVisible);
            }
        }

        void SetPowerPelletsVisible(bool visible)
        {
            // Debug.Log($"{gameObject} SetPowerPelletVisible {visible}, powerPellets.Length: {powerPellets.Length}");
            foreach (Animator powerPellet in powerPellets)
            {
                powerPellet.SetBool("Visible", visible);
            }
        }

        public void SetPowerPelletsBlink(bool enabled)
        {
            // Debug.Log($"{gameObject} SetPowerPelletBlink {enabled}");
            powerPelletBlinkEnabled = enabled;

            powerPelletBlinkCurrentlyVisible = true;
            powerPelletBlinkProgress = 0;
            SetPowerPelletsVisible(true);
        }

        public void FreezePowerPelletsBlink(bool frozen)
        {
            powerPelletBlinkEnabled = !frozen;
        }
        #endregion

        public override void CollectSyncedData(byte[] data, ref int index, NetworkEventType eventType)
        {
            data.Append(pelletCollectedCount, ref index);
            data.Append(syncedPelletsCollected, ref index);

            data.Append(frozen, ref index);
        }

        public override bool WriteSyncedData(byte[] data, ref int index, NetworkEventType eventType)
        {
            pelletCollectedCount = data.ReadByte(ref index);
            Array.Copy(data, index, syncedPelletsCollected, 0, syncedPelletsCollected.Length);
            index += syncedPelletsCollected.Length;
            SetPelletsCollectedFromSync();

            frozen = data.ReadBool(ref index);
            return true;
        }
    }
}