using UnityEngine;

namespace Marro.PacManUdon
{
    enum PacManAnimatorState
    {
        Idle,
        Moving,
        Stopped,
        Big,
        Dead,
    }

    enum PacManState
    {
        Idle,
        Normal,
        PowerPellet,
        Dead,
        Big,
    }

    [RequireComponent(typeof(Animator))]
    [RequireComponent(typeof(Renderer))]
    public class PacMan : GridMover
    {
        private PlayerInput input;
        private CollisionManager collisionManager;
        private Animator animator;
        private new Renderer renderer;

        private Vector2 startPosition;
        private float defaultSpeed;
        private float powerPelletSpeed;

        private PacManState state;

        private float speed;
        private float freezeSeconds;
        private bool frozen;

        private bool kinematic;

        private bool followingPredefinedPath;
        private Direction[] predefinedPath;
        private int predefinedPathIndex;

        private readonly int animatorKeyState = Animator.StringToHash("State");
        private readonly int animatorKeyDirection = Animator.StringToHash("Direction");

        public void Initialize(PlayerInput input, Transform startTransform, GameManager gameManager, CollisionManager collisionManager)
        {
            this.collisionManager = collisionManager;
            this.input = input;
            animator = GetComponent<Animator>();
            renderer = GetComponent<Renderer>();
            frozen = false;
            startPosition = startTransform.localPosition;
        }

        public void Reset()
        {
            SetPosition(startPosition);
            direction = Direction.Left;
            targetDirection = Direction.Left;
            kinematic = false;
            followingPredefinedPath = false;

            state = PacManState.Idle;

            UpdateSpeed();
            UpdateAnimator();
        }

        public override void SyncedUpdate()
        {
            // gameStateManager.statusDisplay.SetDebugText(1, this.targetDirection.ToString());

            if (frozen)
            {
                return;
            }

            float speed = GetNextSpeed();

            Vector2 position = GetPosition();
            Vector2 nextPosition = GetNextPosition(position, directionVectors[(int)direction], speed, networkManager.SyncedDeltaTime); // The position pacman will move to, assuming it doens't get changed

            if (!kinematic)
            {
                if (followingPredefinedPath)
                {
                    nextPosition = ProcessPredefinedPath(position, nextPosition);
                }
                else
                {
                    nextPosition = ProcessNextPosition(position, nextPosition);
                }
            }

            if (CrossesTileBorder(position, nextPosition, direction))
            {
                CheckNewTile(position, nextPosition);
            }

            SetPosition(nextPosition);
        }

        private float GetNextSpeed()
        {
            float speed = this.speed;
            if (freezeSeconds > 0)
            {
                float freezePart = freezeSeconds / networkManager.SyncedDeltaTime;
                if (freezePart >= 1)
                {
                    freezeSeconds -= networkManager.SyncedDeltaTime;
                    animator.speed = 0;
                    speed = 0;
                }
                else
                {
                    speed *= 1 - freezePart;
                    animator.speed = 1 - freezePart;
                    freezeSeconds = 0;
                }
            }
            else
            {
                animator.speed = 1;
            }

            return speed;
        }

        private Vector2 ProcessNextPosition(Vector2 position, Vector2 nextPosition)
        {
            if (CrossesTileCenter(position, nextPosition, Direction.Left) // If pacman is moving horizontally, check if he may cross the center of a tile in that axis
                && (!IsHorizontal(targetDirection) || collisionManager.IsWallUpcoming(nextPosition, directionVectors[(int)HorizontalComponent(direction)])))
            {    // If the target direction is in the other axis or if we're about to run into a wall
                nextPosition.x = PositionToGrid(nextPosition).x; // Snap pacman to the center of his current tile in this axis
                SetDirection(VerticalComponent(direction));
                //Debug.Log($"{gameObject} crossed X tile center from {position}, nextPosition is now {nextPosition} and direction is now {direction}");
            }

            if (CrossesTileCenter(position, nextPosition, Direction.Down) // See comments above but now vertical
                && (!IsVertical(targetDirection) || collisionManager.IsWallUpcoming(nextPosition, directionVectors[(int)VerticalComponent(direction)])))
            {
                nextPosition.y = PositionToGrid(nextPosition).y;
                SetDirection(HorizontalComponent(direction));
                //Debug.Log($"{gameObject} crossed Y tile center from {position} with targetDirection {targetDirection}, nextPosition is now {nextPosition} and direction is now {direction}");
            }

            var inputDirection = input.GetDirection();
            if (!inputDirection.Equals(Direction.Zero) && !inputDirection.Equals(targetDirection) // Ignore neutral input or input in our current direction                                                     
                && !collisionManager.IsWallUpcoming(nextPosition, directionVectors[(int)inputDirection])) // Check if the requested direction does not have a wall
            {
                // Move in the requested direction, as well as perpundicular to it to get to the center of the tunnel
                if (IsHorizontal(inputDirection))
                {
                    var directionToCenter = VerticalToDirection(PositionToGrid(nextPosition).y - nextPosition.y);
                    SetDirection((Direction)((int)inputDirection | (int)directionToCenter));
                }
                else
                {
                    var directionToCenter = HorizontalToDirection(PositionToGrid(nextPosition).x - nextPosition.x);
                    SetDirection((Direction)((int)inputDirection | (int)directionToCenter));
                }
                SetTargetDirection(inputDirection); // This is the direction most logic should assume pacman is moving, the actual direction may be different due to cornering
            }

            return nextPosition;
        }

        private Vector2 ProcessPredefinedPath(Vector2 position, Vector2 nextPosition)
        {
            if (CrossesTileCenter(position, nextPosition, direction))
            {
                // Find the next valid direction which isn't Vector2.zero
                int nextValidDirectionIndex = predefinedPathIndex;
                while (predefinedPath[nextValidDirectionIndex] == Direction.Zero)
                {
                    nextValidDirectionIndex += 1;
                }
                if (!collisionManager.IsWallUpcoming(nextPosition, directionVectors[(int)predefinedPath[nextValidDirectionIndex]]))
                {
                    // If we're at a Vector2.zero, we skip applying the direction and only increment.
                    if (nextValidDirectionIndex == predefinedPathIndex)
                    {
                        SetDirectionAndTargetDirection(predefinedPath[nextValidDirectionIndex]);
                        nextPosition = PositionToGrid(nextPosition) + directionVectors[(int)direction] * 0.01f;

                        // Check if we've reached the end of the path, which includes making sure the path doesn't end on Vector2.zero
                        do
                        {
                            nextValidDirectionIndex += 1;
                            if (nextValidDirectionIndex >= predefinedPath.Length)
                            {
                                followingPredefinedPath = false;
                                break;
                            }
                        } while (predefinedPath[nextValidDirectionIndex] == Direction.Zero);
                    }

                    // gameStateManager.statusDisplay.SetDebugText(1, predefinedPathIndex.ToString());

                    predefinedPathIndex++;
                }
            }
            return nextPosition;
        }

        private void CheckNewTile(Vector2 position, Vector2 nextPosition)
        {
            var eatResult = collisionManager.PacManMoveToTile(position, nextPosition);

            if (eatResult == EatResult.Pellet)
            {
                freezeSeconds = 0.0165f;
            }
            else if (eatResult == EatResult.PowerPellet)
            {
                freezeSeconds = 0.0495f;
            }
        }

        protected override void UpdateAnimator()
        {
            //Debug.Log($"{gameObject} UpdateAnimator with direction {direction}, dead {dead}, frozen {frozen}");
            if (!gameObject.activeInHierarchy)
                return;

            switch (state)
            {
                case PacManState.Dead:
                    SetAnimatorState((int)PacManAnimatorState.Dead);
                    animator.speed = 1;
                    return;
                case PacManState.Idle:
                    SetAnimatorState((int)PacManAnimatorState.Idle);
                    return;
                case PacManState.Big:
                    SetAnimatorState((int)PacManAnimatorState.Big);
                    animator.speed = 1;
                    return;
            }

            if (direction.Equals(Direction.Zero))
            {
                SetAnimatorState((int)PacManAnimatorState.Stopped);
            }
            else
            {
                SetAnimatorState((int)PacManAnimatorState.Moving);
            }

            animator.speed = frozen ? 0 : 1;

            SetAnimatorDirection((int)targetDirection);

        }

        private void SetAnimatorDirection(int value) =>
            animator.SetFloat(animatorKeyDirection, value);

        private void SetAnimatorState(int value) =>
            animator.SetFloat(animatorKeyState, value);

        public void Die()
        {
            state = PacManState.Dead;
            UpdateAnimator();
        }

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

            if (!frozen && state == PacManState.Idle)
            {
                state = PacManState.Normal;
            }

            UpdateAnimator();
        }

        public void SetLevel(int level)
        {
            // Debug.Log($"{gameObject} SetLevel {level}");
            defaultSpeed = PacManConstants.GetPacManDefaultSpeedForLevel(level);
            powerPelletSpeed = PacManConstants.GetPacManPowerPelletSpeedForLevel(level);
            UpdateSpeed();
        }

        public void SetPowerPellet(bool powerPellet)
        {
            state = powerPellet ? PacManState.PowerPellet : PacManState.Normal;
            UpdateSpeed();
        }

        private void UpdateSpeed()
        {
            if (state == PacManState.PowerPellet)
            {
                speed = powerPelletSpeed;
            }
            else
            {
                speed = defaultSpeed;
            }
        }

        public void SetActive(bool active)
        {
            gameObject.SetActive(active);
            renderer.enabled = active;
            if (active)
            {
                UpdateAnimator();
            }
        }

        public void SetKinematic(bool kinematic)
        {
            this.kinematic = kinematic;
        }

        public void SetPredefinedPath(Direction[] predefinedPath)
        {
            this.predefinedPath = predefinedPath;
            followingPredefinedPath = true;
            predefinedPathIndex = 0;
        }

        public void BecomeBig()
        {
            state = PacManState.Big;
            UpdateAnimator();
        }

        public void SetVisibility(bool visible)
        {
            renderer.enabled = visible;
        }

        public override void CollectSyncedData(byte[] data, ref int index, NetworkEventType eventType)
        {
            data.AppendAsByte((int)state, ref index);
            data.Append(freezeSeconds, ref index);
            data.Append(frozen, ref index);

            base.CollectSyncedData(data, ref index, eventType);
        }

        public override bool WriteSyncedData(byte[] data, ref int index, NetworkEventType eventType)
        {
            state = (PacManState)data.ReadByte(ref index);
            freezeSeconds = data.ReadFloat(ref index);
            frozen = data.ReadBool(ref index);

            UpdateSpeed();

            return base.WriteSyncedData(data, ref index, eventType);
        }
    }
}