namespace Marro.PacManUdon
{
    using System;
    using UdonSharp;
    using UnityEngine;
    using VRC.SDKBase;
    using VRC.Udon;
    using VRC.SDK3.Components;
    
    public class PacMan : GridMover
    {
        private GameManager gameController;
        private PlayerInput input;
        private float defaultSpeed;
        private float powerPelletSpeed;
        private float speed;
        private Vector3 startPosition;
        private Quaternion startRotation;
        private Vector3 startScale;
        private Animator animator;
        new Renderer renderer;
        private VRCObjectPool pelletPool;
        private bool hideUntilUnfrozen;
        private bool dead;
        private bool kinematic;

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

        [UdonSynced] private Vector2 syncedPosition;
        [UdonSynced] private Vector2 targetDirection;
        [UdonSynced] private Vector2 direction;
        [UdonSynced] private float freezeSeconds;
        [UdonSynced] private bool frozen;

#region Animator constants
        private const string AnimatorKeyDead = "Dead";
        private const string AnimatorKeyDirection = "Direction";
        private const float AnimatorDirectionNone = 0f;
        private const float AnimatorDirectionRight = 0.25f;
        private const float AnimatorDirectionLeft = 0.50f;
        private const float AnimatorDirectionUp = 0.75f;
        private const float AnimatorDirectionDown = 1f;
        private const float AnimatorDirectionRightBig = 1.25f;
#endregion


        public void Initialize(PlayerInput input, VRCObjectPool pelletPool, GameManager gameController)
        {
            this.gameController = gameController;
            this.input = input;
            this.pelletPool = pelletPool;
            animator = GetComponent<Animator>();
            renderer = GetComponent<Renderer>();
            frozen = false;
            hideUntilUnfrozen = false;
            startPosition = transform.localPosition;
            startRotation = transform.localRotation;
            startScale = transform.localScale;
        }

        public void Reset()
        {
            // Debug.Log($"{gameObject} Reset!");
            transform.localPosition = startPosition;
            transform.localRotation = startRotation;
            transform.localScale = startScale;
            direction = Vector2.left;
            targetDirection = Vector2.left;
            speed = defaultSpeed;
            kinematic = false;
            followingPredefinedPath = false;

            SetDead(false);
            animator.SetTrigger("Reset");
        }

        void FixedUpdate()
        {
            // gameStateManager.statusDisplay.SetDebugText(1, this.targetDirection.ToString());

            if (frozen)
            {
                return;
            }

            if (hideUntilUnfrozen)
            {
                hideUntilUnfrozen = false;
                SetVisibility(true);
            }

            float speed = this.speed;
            if (freezeSeconds > 0)
            {
                float freezePart = freezeSeconds / Time.deltaTime;
                if (freezePart >= 1)
                {
                    freezeSeconds -= Time.deltaTime;
                    animator.speed = 0;
                    return;
                }
                speed *= 1 - freezePart;
                animator.speed = 1 - freezePart;
                freezeSeconds = 0;
            }
            else
            {
                animator.speed = 1;
            }

            Vector2 position = GetPosition();
            Vector2 nextPosition = GridMoverTools.GetNextPosition(position, direction, speed); // 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);
                }
            }

            SetPosition(nextPosition);
        }

        private Vector2 ProcessNextPosition(Vector2 position, Vector2 nextPosition)
        {
            if (GridMoverTools.CrossesTileCenter(position, nextPosition, direction.x != 0, false)                                           // If pacman is moving horizontally, check if he may cross the center of a tile in that axis
                && (targetDirection.x == 0 || GridMoverTools.CheckCollisionInDirection(transform, nextPosition, new Vector2(direction.x, 0))))
            {    // If the target direction is in the other axis or if we're about to run into a wall
                nextPosition.x = GridMoverTools.PositionToGrid(nextPosition).x;                                                                 // Snap pacman to the center of his current tile in this axis
                SetDirection(new Vector2(0, direction.y));
                // Debug.Log($"{gameObject} crossed X tile center from {currentPosition}, nextPosition is now {nextPosition} and direction is now {direction}");
            }

            if (GridMoverTools.CrossesTileCenter(position, nextPosition, false, direction.y != 0)                                           // See comments above but now vertical
            && (targetDirection.y == 0 || GridMoverTools.CheckCollisionInDirection(transform, nextPosition, new Vector2(0, direction.y))))
            {
                nextPosition.y = GridMoverTools.PositionToGrid(nextPosition).y;
                SetDirection(new Vector2(direction.x, 0));
                // Debug.Log($"{gameObject} crossed Y tile center from {currentPosition} with targetDirection {targetDirection}, nextPosition is now {nextPosition} and direction is now {direction}");
            }

            if (Networking.IsOwner(gameObject))
            {
                Vector2 inputDirection = input.GetDirection();
                if (!inputDirection.Equals(Vector2.zero) && !inputDirection.Equals(targetDirection)                                                      // Ignore neutral input or input in our current direction
                && (inputDirection.x == 0 || (Math.Round(nextPosition.y, 5) - 0.5) % 1 != 0) && (inputDirection.y == 0 || (Math.Round(nextPosition.x, 5) - 0.5) % 1 != 0)   // Target grid position near the edge of a tile may not be correct, ignore inputs near the border
                && !GridMoverTools.CheckCollisionInDirection(transform, nextPosition, inputDirection))
                {                                                // Check if the requested direction does not have a wall
                    if (inputDirection.x != 0)
                    {         // Move in the requested direction, as well as perpundicular to it to get to the center of the tunnel
                        SetDirection(inputDirection + new Vector2(0, GridMoverTools.PositionToGrid(nextPosition).y - nextPosition.y).normalized);
                    }
                    else
                    {
                        SetDirection(inputDirection + new Vector2(GridMoverTools.PositionToGrid(nextPosition).x - nextPosition.x, 0).normalized);
                    }
                    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 (GridMoverTools.CrossesTileCenter(position, nextPosition, direction.x != 0, direction.y != 0))
            {
                // Find the next valid direction which isn't Vector2.zero
                int nextValidDirectionIndex = predefinedPathIndex;
                while (predefinedPath[nextValidDirectionIndex] == Vector2.zero)
                {
                    nextValidDirectionIndex += 1;
                }
                if (!GridMoverTools.CheckCollisionInDirection(transform, nextPosition, predefinedPath[nextValidDirectionIndex]))
                {
                    // If we're at a Vector2.zero, we skip applying the direction and only increment.
                    if (nextValidDirectionIndex == predefinedPathIndex)
                    {
                        SetDirection(predefinedPath[nextValidDirectionIndex]);
                        SetTargetDirection(predefinedPath[nextValidDirectionIndex]);
                        nextPosition = GridMoverTools.PositionToGrid(nextPosition) + direction.normalized * 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] == Vector2.zero);
                    }

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

                    predefinedPathIndex++;
                }
            }
            return nextPosition;
        }

        private void UpdateAnimator()
        {
            // Debug.Log($"{gameObject} UpdateAnimator with direction {direction}, dead {dead}, frozen {frozen}");
            if (!gameObject.activeInHierarchy)
                return;
            
            animator.SetBool(AnimatorKeyDead, dead);
            if (dead)
            {
                animator.speed = 1;
                return;
            }

            if (frozen || direction.Equals(Vector2.zero))
            {
                animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionNone);
                animator.speed = 0;
            }
            else
            {
                animator.speed = 1;
                if (targetDirection.Equals(Vector2.right))
                {
                    animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionRight);
                }
                else if (targetDirection.Equals(Vector2.left))
                {
                    animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionLeft);
                }
                else if (targetDirection.Equals(Vector2.up))
                {
                    animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionUp);
                }
                else if (targetDirection.Equals(Vector2.down))
                {
                    animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionDown);
                }
            }
        }

        public void SetDead(bool dead)
        {
            this.dead = dead;
            UpdateAnimator();
        }

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

        public void HideUntilUnfrozen()
        {
            hideUntilUnfrozen = true;
            SetVisibility(false);
        }

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

        public void SetPowerPellet(bool powerPellet)
        {
            if (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(Vector2[] predefinedPath)
        {
            this.predefinedPath = predefinedPath;
            followingPredefinedPath = true;
            predefinedPathIndex = 0;
        }

        public void BecomeBig()
        {
            animator.SetFloat(AnimatorKeyDirection, AnimatorDirectionRightBig);
        }

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

        public override Vector2 GetPosition()
        {
            return (Vector2)transform.localPosition;
        }

        public override void SetPosition(Vector2 position)
        {
            GridMoverTools.SetPosition(position, transform);
        }

        public override Vector2 GetDirection()
        {
            return direction;
        }

        public void SetDirection(Vector2 direction)
        {
            this.direction = direction;
            RequestSerialization();
            UpdateAnimator();
        }

        public void SetTargetDirection(Vector2 targetDirection)
        {
            this.targetDirection = targetDirection;
            UpdateAnimator();
        }

        public override void OnPreSerialization()
        {
            syncedPosition = GetPosition();
        }

        public override void OnDeserialization()
        {
            SetPosition(syncedPosition);
            UpdateAnimator();
        }

        void OnTriggerEnter(Collider other)
        {
            Pellet pellet = other.gameObject.GetComponent<Pellet>();
            if (pellet)
            {
                if (Networking.IsOwner(gameObject))
                {
                    pelletPool.Return(pellet.gameObject);
                }
                else
                {
                    pellet.pelletRenderer.enabled = false;
                    pellet.gameObject.SetActive(false);
                }

                if (pellet.isPowerPellet)
                {
                    if (Networking.IsOwner(gameObject)) gameController.GotPowerPellet();
                    freezeSeconds = 0.05f;
                }
                else
                {
                    if (Networking.IsOwner(gameObject)) gameController.GotPellet();
                    freezeSeconds = 0.0166666666666667f;
                }

                if (Networking.IsOwner(gameObject)) RequestSerialization();
                return;
            }
            else if (Networking.IsOwner(gameObject) && other.gameObject.GetComponent<BonusFruit>())
            {
                gameController.GotFruit();
            }
        }
    }
}