#define RECORDING_DEMO

namespace Marro.PacManUdon
{
    using System;
    using UdonSharp;
    using UnityEngine;
    using VRC.SDKBase;
    using VRC.Udon;
    using VRC.SDK3.Components;
    using VRC.Udon.Common.Interfaces;
    using VRC.SDK3.Data;

    public class GameController : UdonSharpBehaviour
    {
        [Header("Static game components")]
        [SerializeField] private Maze[] mazes;
        [SerializeField] private PacMan pacMan;
        [SerializeField] private GhostManager ghostManager;
        [SerializeField] private BonusFruit bonusFruit;
        [SerializeField] private PelletManager pelletManager;
        [SerializeField] public StatusDisplay statusDisplay; // This one is public so other scripts can write to the debug display
        [SerializeField] private PelletManager attractScreen;
        [SerializeField] private GameObject pressStartButtonScreen;
        [SerializeField] private PlayerInput playerInput;
        [SerializeField] private Animator demo;
        [SerializeField] private SoundManager soundManager;

        [SerializeField] private GameObject recorder;


        [Header("Game settings")]
        [SerializeField] private int startingExtraLives = 3;
        [SerializeField] private int scoreToExtraLife = 10000;
        [Tooltip("Override amount of pellets needed to clear stage, set to -1 to disable.")]
        [SerializeField] private int pelletCountOverride = -1;

        private Maze maze;
        private VRCObjectPool pelletPool;

        private Animator mazeSpriteAnimator;
        private int pelletCountTotal;
        private int pelletCountRemaining;
        private GameObject[] attractScreenElements;

        [UdonSynced, FieldChangeCallback(nameof(GameState))] private PacManGameState gameState;
        [UdonSynced, FieldChangeCallback(nameof(Score))] private int score;
        [UdonSynced, FieldChangeCallback(nameof(Level))] private int level;
        [UdonSynced, FieldChangeCallback(nameof(HighScore))] private int highScore;
        [UdonSynced, FieldChangeCallback(nameof(ExtraLives))] private int extraLives;

        public void Start()
        {
            maze = mazes[0];
            pelletPool = maze.pelletContainer.GetComponent<VRCObjectPool>();
            mazeSpriteAnimator = maze.mazeSprite.GetComponent<Animator>();
            attractScreenElements = new GameObject[attractScreen.transform.childCount];
            for (int i = 0; i < attractScreenElements.Length; i++)
            {
                attractScreenElements[i] = attractScreen.transform.GetChild(i).gameObject;
            }

            ghostManager.Initialize(maze.ghostTargets, pacMan, this);
            pacMan.Initialize(playerInput, pelletPool, this);
            bonusFruit.Initialize();
            pelletManager.Initialize(pelletPool);
            statusDisplay.Initialize();
            playerInput.Initialize(this);
            soundManager.Initialize();

            SetScore(0);
            SetHighScore(0);
            SetLevel(0);

            StartAttractMode();
        }

        public void FixedUpdate()
        {
            TimeSequenceUpdate(Time.deltaTime);
        }

        public void JoystickGrabbed()
        {
            if (gameState == PacManGameState.AttractMode || gameState == PacManGameState.AttractModeDemo)
                StartTimeSequence(PacManTimeSequence.WaitForStart);
        }

        public void JoystickReleased()
        {
            if (gameState == PacManGameState.WaitForStart)
                StartTimeSequence(PacManTimeSequence.WaitForStartTimeout);
        }

        public void StartGameButtonPressed()
        {
            Debug.Log($"{gameObject} Start Game Button was pressed!");
            TakeOwnership();
            StartTimeSequence(PacManTimeSequence.StartNewGame);
        }

        public void SkipLevelButtonPressed()
        {
            if (Networking.IsOwner(gameObject))
            {
                Debug.Log($"{gameObject} Skip level button pressed!");
                StartTimeSequence(PacManTimeSequence.BoardClear);
                TimeSequenceSkipToNextStep();
            }
        }

        public void StartDemoButtonPressed()
        {
            if (Networking.IsOwner(gameObject))
            {
                Debug.Log($"{gameObject} Start demo button pressed!");
                StartTimeSequence(PacManTimeSequence.AttractScreenDemo);
            }
        }

        private void StartAttractMode()
        {
#if RECORDING_DEMO
            // recorder.gameObject.SetActive(true);
            StartTimeSequence(PacManTimeSequence.AttractScreenIntroduction);
#else
            SetGameState(PacManGameState.AttractMode);
            HideEverything();
            demo.gameObject.SetActive(true);
#endif
        }

        private void InitializeNewGame()
        {
            Debug.Log($"{gameObject} Started new game!");
            SetScore(0);
            SetExtraLives(startingExtraLives);
            SetLevel(1);
        }

        private void InitializeLevel()
        {
            Debug.Log($"{gameObject} New level started!");

            if (Networking.IsOwner(gameObject))
            {
                pelletCountTotal = pelletPool.Pool.Length;
                pelletCountRemaining = pelletCountTotal;
                ghostManager.SetPelletsRemaining(pelletCountRemaining);
                ghostManager.NewLevel();

                pelletManager.RestoreAllPellets();

                if (pelletCountOverride > 0)
                {
                    pelletCountRemaining = pelletCountOverride;
                }
            }
            mazeSpriteAnimator.SetBool("Blinking", false);
        }

        private void RestartLevel()
        {
            Debug.Log($"{gameObject} (Re)started level!");

            // SetInGameComponentVisibility(true);

            ghostManager.Reset();
            pacMan.Reset();
            bonusFruit.Despawn();
            soundManager.Reset();
            pelletManager.SetPowerPelletsBlink(false);
        }

        public void GotPellet(bool addScore = true)
        {
            pelletCountRemaining--;
            if (addScore) AddScore(10);

            ghostManager.PelletConsumed();

            soundManager.PlayPelletSound();

            soundManager.UpdatePelletCount(pelletCountRemaining);

            int pelletsConsumed = pelletCountTotal - pelletCountRemaining;
            if (pelletCountRemaining <= 0)
            {
                StartTimeSequence(PacManTimeSequence.BoardClear);
            }
            else if (pelletsConsumed == 70 || pelletsConsumed == 170)
            {
                bonusFruit.Spawn();
            }
        }

        public void GotPowerPellet()
        {
            if (gameState == PacManGameState.AttractMode)
            {
                TimeSequenceSkipToNextStep();
                return;
            }
            GotPellet(addScore: false);
            AddScore(50);
            ghostManager.SetPowerPellet(true);
            pacMan.SetPowerPellet(true);
            soundManager.SetGhostBlue(true);
        }

        public void EndPowerPellet()
        {
            ghostManager.SetPowerPellet(false);
            pacMan.SetPowerPellet(false);
            soundManager.SetGhostBlue(false);
        }

        public void GotFruit()
        {
            AddScore(bonusFruit.Collected());
            soundManager.PlayFruitSound();
        }

        public void GhostCaught(int scoreBonus)
        {
            if (gameState == PacManGameState.AttractMode)
            {
                TimeSequenceSkipToNextStep();
                return;
            }

            AddScore(scoreBonus);
            StartTimeSequence(PacManTimeSequence.GhostCaught);
            pacMan.HideUntilUnfrozen();
        }

        public void PacManCaught()
        {
            StartTimeSequence(PacManTimeSequence.PacManCaught);
        }

        public void NoGhostsScared()
        {
            soundManager.SetGhostBlue(false);
        }

        public void NoGhostsRetreating()
        {
            soundManager.SetGhostRetreat(false);
        }

        void BoardClearAnimation()
        {
            ghostManager.gameObject.SetActive(false);
            mazeSpriteAnimator.SetBool("Blinking", true);
        }

        
        private void HideEverything()
        {
            SetMazeActive(false);
            SetPelletsActive(false);
            SetMazeVisible(false);
            SetGhostsActive(false);
            SetPacManActive(false);
            SetPressStartButtonScreenVisible(false);
            statusDisplay.SetGameOverTextVisible(false);
            statusDisplay.SetExtraLivesDisplayVisible(false);
            statusDisplay.SetLevelDisplayVisible(false);
            statusDisplay.SetPlayer1TextVisible(false);
            statusDisplay.SetReadyTextVisible(false);
            demo.gameObject.SetActive(false);
        }

        void SetMazeActive(bool active)
        {
            maze.gameObject.SetActive(active);
        }

        void SetPelletsActive(bool active)
        {
            pelletPool.gameObject.SetActive(active);
        }

        void SetMazeVisible(bool visible)
        {
            mazeSpriteAnimator.SetBool("Hidden", !visible);
        }

        void SetGhostsActive(bool active)
        {
            ghostManager.SetActive(active);
        }

        void SetPacManActive(bool active)
        {
            pacMan.SetActive(active);
        }

        void SetPressStartButtonScreenVisible(bool visible)
        {
            pressStartButtonScreen.SetActive(visible);
        }

        void SetGameState(PacManGameState newGameState)
        {
            // Debug.Log($"{gameObject} State transitioning from {gameState} to {newGameState}");
            gameState = newGameState;
            if (Networking.IsOwner(gameObject))
            {
                RequestSerialization();
            }
        }

        private void IncrementLevel()
        {
            SetLevel(level + 1);
        }

        private void SetLevel(int level)
        {
            this.level = level;
            pacMan.SetLevel(level);
            ghostManager.SetLevel(level);
            statusDisplay.SetLevel(level);
            bonusFruit.SetFruitType(PacManConstants.GetFruitTypeForLevel(level));
        }

        void AddScore(int score)
        {
            if (gameState == PacManGameState.AttractMode || gameState == PacManGameState.AttractModeDemo)
            {
                return;
            }

            if (this.score < scoreToExtraLife && this.score + score >= scoreToExtraLife)
            {
                BonusLifeReached();
            }

            SetScore(this.score + score);
            RequestSerialization();
        }

        void SetScore(int score)
        {
            this.score = score;
            statusDisplay.Set1UPScore(score);
            if (score > highScore)
            {
                highScore = score;
                statusDisplay.SetHighScore(score);
            }
        }

        void SetHighScore(int highScore)
        {
            this.highScore = highScore;
            statusDisplay.SetHighScore(score);
        }

        public void DecrementLives()
        {
            if (!Networking.IsOwner(gameObject))
            {
                return;
            }
            // Debug.Log($"{gameObject} Decremented lives from {extraLives} to {extraLives - 1}");
            SetExtraLives(extraLives - 1);
        }

        void IncrementLives()
        {
            if (!Networking.IsOwner(gameObject))
            {
                return;
            }
            // Debug.Log($"{gameObject} Incremented lives from {extraLives} to {extraLives + 1}");
            SetExtraLives(extraLives + 1);
        }

        void SetExtraLives(int extraLives)
        {
            // Debug.Log($"{gameObject} Set lives from {this.extraLives} to {extraLives}");
            this.extraLives = extraLives;
            statusDisplay.SetExtraLives(extraLives);
        }

        void BonusLifeReached()
        {
            IncrementLives();
            soundManager.PlayExtraLifeSound();
        }

        public void SetFrozen(bool frozen, bool ghostIgnoreIfCaught = false, bool ghostKeepAnimating = false)
        {
            // Debug.Log($"{gameObject} Set Frozen: {frozen}");
            pacMan.SetFrozen(frozen);
            bonusFruit.SetFrozen(frozen);
            ghostManager.SetFrozen(frozen, ignoreIfCaught: ghostIgnoreIfCaught);

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

        void TakeOwnership()
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
            Networking.SetOwner(Networking.LocalPlayer, pacMan.gameObject);
            Networking.SetOwner(Networking.LocalPlayer, pelletPool.gameObject);
            ghostManager.SetOwner(Networking.LocalPlayer);
        }

        public int ExtraLives
        {
            set
            {
                SetExtraLives(value);
            }
            get => extraLives;
        }

        public PacManGameState GameState
        {
            set
            {
                SetGameState(value);
            }
            get => gameState;
        }

        public bool GhostsScared
        {
            set
            {

            }
            get => GhostsScared;
        }

        public int Score
        {
            set
            {
                SetScore(value);
            }
            get => score;
        }

        public int HighScore
        {
            set
            {
                SetHighScore(value);
            }
            get => score;
        }

        public int Level
        {
            set
            {
                SetLevel(value);
            }
            get => level;
        }

#region TIME SEQUENCE BEHAVIOUR
        // This was supposed to be a separate class, right until the moment I realized Udon doesn't support instantiating classes...

        bool currentlyInTimeSequence;
        PacManTimeSequence currentTimeSequence;
        bool hasTimeSequenceQueued;
        private DataList timeSequenceQueue;
        [UdonSynced] float timeSequenceSecondsPassed;
        int timeSequenceProgress;
        float[] timeSequenceKeyframeTimes;

        private void StartTimeSequence(PacManTimeSequence timeSequence)
        {
            if (timeSequenceQueue == null)
            {
                timeSequenceQueue = new DataList();
            }

            if (currentlyInTimeSequence == true)
            {
                int timeSequenceInt = (int)timeSequence; // Doing the conversion in the line below crashes the script. I love working in Udon
                timeSequenceQueue.Add(timeSequenceInt);
                hasTimeSequenceQueued = true;

                return;
            }

            Debug.Log($"StartTimeSequence: {timeSequence}");

            currentlyInTimeSequence = true;
            currentTimeSequence = timeSequence;
            timeSequenceProgress = 0;
            timeSequenceSecondsPassed = 0;
            timeSequenceKeyframeTimes = GetTimeSequenceKeyframeTimes(timeSequence);
            TimeSequenceProgressToTime(timeSequenceSecondsPassed);
        }

        private void InsertTimeSequence(PacManTimeSequence timeSequence)
        {
            StartTimeSequence(timeSequence);
        }

        private void TimeSequenceUpdate(float deltaSeconds)
        {
            if (!currentlyInTimeSequence && hasTimeSequenceQueued)
            {
                timeSequenceQueue.TryGetValue(0, out DataToken nextTimeSequence);
                StartTimeSequence((PacManTimeSequence)nextTimeSequence.Int);
                timeSequenceQueue.RemoveAt(0);
                hasTimeSequenceQueued = timeSequenceQueue.Count > 0 ? true : false;
                return;
            }

            if (currentlyInTimeSequence)
            {
                if (hasTimeSequenceQueued)
                {
                    while (currentlyInTimeSequence)
                    {
                        TimeSequenceSkipToNextStep();
                    }
                }
                else
                {
                    TimeSequenceProgressToTime(timeSequenceSecondsPassed + deltaSeconds);
                }
            }
        }

        private void TimeSequenceSkipToNextStep()
        {
            // Debug.Log($"{gameObject} TimeSequenceSkipToNextStep");
            if (timeSequenceProgress < timeSequenceKeyframeTimes.Length)
            {
                TimeSequenceProgressToTime(timeSequenceKeyframeTimes[timeSequenceProgress]);
            }
            else
            {
                Debug.LogWarning($"{gameObject} Tried skipping to next time sequence step when already on last step!");
                currentlyInTimeSequence = false;
            }
        }

        private void TimeSequenceProgressToTime(float seconds)
        {
            timeSequenceSecondsPassed = seconds;
            while (timeSequenceSecondsPassed >= timeSequenceKeyframeTimes[timeSequenceProgress])
            {
                TimeSequenceExecuteStep(timeSequenceProgress);

                timeSequenceProgress += 1;
                if (timeSequenceProgress >= timeSequenceKeyframeTimes.Length)
                {
                    currentlyInTimeSequence = false;
                    break;
                }
            }
        }

        private void TimeSequenceExecuteStep(int sequenceProgress)
        {
            // Debug.Log($"{gameObject} Triggered time sequence step for sequence {currentTimeSequence} with progress {sequenceProgress}");
            switch (currentTimeSequence)
            {
                default:
                    Debug.LogError($"{gameObject} No time sequence keyframes known for sequence {currentTimeSequence}");
                    break;
                case PacManTimeSequence.AttractScreenIntroduction:
                    TimeSequenceStepAttractScreenIntroduction(sequenceProgress);
                    break;
                case PacManTimeSequence.AttractScreenDemo:
                    TimeSequenceStepAttractScreenDemo(sequenceProgress);
                    break;
                case PacManTimeSequence.WaitForStart:
                    TimeSequenceStepWaitForStart(sequenceProgress);
                    break;
                case PacManTimeSequence.WaitForStartTimeout:
                    TimeSequenceStepWaitForStartTimeout(sequenceProgress);
                    break;
                case PacManTimeSequence.StartNewGame:
                    TimeSequenceStepStartNewGame(sequenceProgress);
                    break;
                case PacManTimeSequence.BoardClear:
                    TimeSequenceStepBoardClear(sequenceProgress);
                    break;
                case PacManTimeSequence.StartNewLevel:
                    TimeSequenceStepStartNewLevel(sequenceProgress);
                    break;
                case PacManTimeSequence.GhostCaught:
                    TimeSequenceStepGhostCaught(sequenceProgress);
                    break;
                case PacManTimeSequence.PacManCaught:
                    TimeSequenceStepPacManCaught(sequenceProgress);
                    break;
                case PacManTimeSequence.RestartLevel:
                    TimeSequenceStepRestartLevel(sequenceProgress);
                    break;
                case PacManTimeSequence.GameOver:
                    TimeSequenceStepGameOver(sequenceProgress);
                    break;
            }
        }

        private float[] GetTimeSequenceKeyframeTimes(PacManTimeSequence timeSequence)
        {
            switch (timeSequence)
            {
                default:
                    Debug.LogError($"{gameObject} No time sequence keyframe times known for sequence {timeSequence}");
                    return new float[0];
                case PacManTimeSequence.AttractScreenIntroduction:
                    return DeltaToAbsolute(new float[] { 0, 0.032f, 1f, 1f, .5f, .5f, 1f, .5f, .5f, 1f, .5f, .5f, 1f, .5f, 1f, 1f, 1f,
                                                     5f, 0.2f, 2f, 0.91667f, 2f, 0.91667f, 2f, 0.91667f, 2f, 0.91667f });
                case PacManTimeSequence.AttractScreenDemo:
                    return DeltaToAbsolute(new float[] { 0, 0.016f, 0.05f, 0.16f, 0.33f, 1.85f, 54f });
                case PacManTimeSequence.WaitForStart:
                    return DeltaToAbsolute(new float[] { 0, 0.016f });
                case PacManTimeSequence.WaitForStartTimeout:
                    return DeltaToAbsolute(new float[] { 0, 5f });
                case PacManTimeSequence.StartNewGame:
                    return DeltaToAbsolute(new float[] { 0, 0.016f, 2.2f, 0.032f, 0.032f, 1.92f, 0.032f });
                case PacManTimeSequence.BoardClear:
                    return DeltaToAbsolute(new float[] { 0, 2f, 0.016f, 1.6f - 0.016f, 0.016f, 0.032f, 0.3f });
                case PacManTimeSequence.StartNewLevel:
                    return DeltaToAbsolute(new float[] { 0, 0.064f, 0.032f, 1.85f, 0.016f });
                case PacManTimeSequence.GhostCaught:
                    return DeltaToAbsolute(new float[] { 0, 0.91667f });
                case PacManTimeSequence.PacManCaught:
                    return DeltaToAbsolute(new float[] { 0, 1, 0.35f, 2.40f, 2f });
                case PacManTimeSequence.RestartLevel:
                    return DeltaToAbsolute(new float[] { 0, 0.016f, 0.064f, 0.032f, 1.85f, 0.016f });
                case PacManTimeSequence.GameOver:
                    return DeltaToAbsolute(new float[] { 0, 1.95f });
            }
        }

        private static float[] DeltaToAbsolute(float[] delta)
        {
            if (delta.Length < 1)
            {
                return new float[0];
            }

            float[] absolute = new float[delta.Length];
            absolute[0] = delta[0];
            for (int i = 1; i < delta.Length; i++)
            {
                absolute[i] = delta[i] + absolute[i - 1];
            }
            return absolute;
        }

        private void TimeSequenceStepAttractScreenIntroduction(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    SetGameState(PacManGameState.AttractMode);
                    
                    // Initialize
                    soundManager.SuppressSound(true);
                    RestartLevel();
                    HideEverything();
                    SetFrozen(true);
                    attractScreen.gameObject.SetActive(true);
                    attractScreen.Initialize();
                    for (int i = 0; i <= 15; i++)
                    {
                        // Debug.Log($"{gameObject} TimeSequenceAttractScreen deactivating with iteration i");
                        attractScreenElements[i].SetActive(false);
                    }
                    attractScreen.SetPowerPelletsBlink(false);
                    break;
                case 1:
                    // show "Character / Nickname"
                    attractScreenElements[0].SetActive(true);
                    break;
                case 2:
                    // Show blinky sprite
                    attractScreenElements[1].SetActive(true);
                    break;
                case 3:
                    // Show blinky character
                    attractScreenElements[2].SetActive(true);
                    break;
                case 4:
                    // Show blinky nickname
                    attractScreenElements[3].SetActive(true);
                    break;
                case 5:
                    // Show pinky sprite
                    attractScreenElements[4].SetActive(true);
                    break;
                case 6:
                    // Show pinky character
                    attractScreenElements[5].SetActive(true);
                    break;
                case 7:
                    // Show pinky nickname
                    attractScreenElements[6].SetActive(true);
                    break;
                case 8:
                    // Show inky sprite
                    attractScreenElements[7].SetActive(true);
                    break;
                case 9:
                    // Show inky character
                    attractScreenElements[8].SetActive(true);
                    break;
                case 10:
                    // Show inky nickname
                    attractScreenElements[9].SetActive(true);
                    break;
                case 11:
                    // Show clyde sprite
                    attractScreenElements[10].SetActive(true);
                    break;
                case 12:
                    // Show clyde character
                    attractScreenElements[11].SetActive(true);
                    break;
                case 13:
                    // Show clyde nickname
                    attractScreenElements[12].SetActive(true);
                    break;
                case 14:
                    // Show pellet point values
                    attractScreenElements[13].SetActive(true);
                    break;
                case 15:
                    // Show copyright message, setup pellet demonstration
                    attractScreenElements[14].SetActive(true);
                    attractScreenElements[15].SetActive(true);

                    pacMan.SetLevel(1);
                    pacMan.Reset();
                    pacMan.SetKinematic(true);
                    pacMan.SetActive(true);
                    pacMan.SetPosition(attractScreenElements[16].transform.localPosition);
                    pacMan.SetDirection(Vector2.left);

                    ghostManager.SetLevel(2);
                    ghostManager.Reset();
                    ghostManager.SetKinematic(true);
                    ghostManager.SetActive(true);
                    Ghost[] ghosts = ghostManager.Ghosts;
                    for (int i = 0; i < ghosts.Length; i++)
                    {
                        ghosts[i].SetPosition(attractScreenElements[17 + i].transform.localPosition);
                        ghosts[i].SetDirection(Vector2.left);
                        ghosts[i].SetState(PacManGhostState.Normal);
                    }
                    break;

                case 16:
                    attractScreen.SetPowerPelletsBlink(true);

                    SetFrozen(false);
                    break;

                case 17:
                    ghostManager.SetPowerPellet(true);
                    pacMan.SetPowerPellet(true);
                    attractScreenElements[15].SetActive(false);
                    break;

                case 18:
                    // Turn PacMan around after eating power pellet
                    pacMan.SetDirection(Vector2.right);
                    pacMan.SetTargetDirection(Vector2.right);
                    break;

                case 19:
                    ghostManager.Ghosts[0].Caught(200);
                    pacMan.SetActive(false);
                    SetFrozen(true);
                    break;
                case 20:
                    ghostManager.Ghosts[0].ReturnHome();
                    ghostManager.Ghosts[0].SetActive(false);
                    pacMan.SetActive(true);
                    SetFrozen(false);
                    break;
                case 21:
                    ghostManager.Ghosts[1].Caught(400);
                    pacMan.SetActive(false);
                    SetFrozen(true);
                    break;
                case 22:
                    ghostManager.Ghosts[1].ReturnHome();
                    ghostManager.Ghosts[1].SetActive(false);
                    pacMan.SetActive(true);
                    SetFrozen(false);
                    break;
                case 23:
                    ghostManager.Ghosts[2].Caught(800);
                    pacMan.SetActive(false);
                    SetFrozen(true);
                    break;
                case 24:
                    ghostManager.Ghosts[2].ReturnHome();
                    ghostManager.Ghosts[2].SetActive(false);
                    pacMan.SetActive(true);
                    SetFrozen(false);
                    break;
                case 25:
                    ghostManager.Ghosts[3].Caught(1600);
                    pacMan.SetActive(false);
                    SetFrozen(true);
                    break;
                case 26:
                    ghostManager.Ghosts[3].ReturnHome();
                    ghostManager.Ghosts[3].SetActive(false);
                    // Hide elements, start demo
                    attractScreen.gameObject.SetActive(false);

                    if (!hasTimeSequenceQueued)
                    {
                        StartTimeSequence(PacManTimeSequence.AttractScreenDemo);
                    }
                    break;
            }
        }

        private void TimeSequenceStepAttractScreenDemo(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    SetGameState(PacManGameState.AttractModeDemo);
                    HideEverything();
                    SetFrozen(true);
                    break;
                case 1:
                    InitializeLevel();
                    SetMazeActive(true);
                    SetMazeVisible(true);
                    SetLevel(1);
                    break;
                case 2:
                    // Reset ghosts
                    RestartLevel();

                    ghostManager.Ghosts[0].SetPredefinedPath(new Vector2[]{ // Blinky
                        Vector2.down,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.right,
                        Vector2.down,
                        Vector2.zero,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.up,
                        Vector2.zero,
                        Vector2.left,
                        Vector2.down,
                        Vector2.zero,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        // Goes through tunnel
                        Vector2.zero,
                        Vector2.up,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.left,
                        // Gets eaten
                        Vector2.zero,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                    });

                    ghostManager.Ghosts[1].SetPredefinedPath(new Vector2[]{ // Pinky
                        Vector2.down,
                        Vector2.left,
                        Vector2.up,
                        Vector2.zero,
                        Vector2.zero,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        // Pellet starts
                        Vector2.up,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        // Pellet ends
                        Vector2.zero,
                        Vector2.down,
                        // Pellet starts
                        Vector2.left,
                        Vector2.down,
                        Vector2.zero,
                        Vector2.left,
                        // Caught
                        Vector2.up,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        // Home
                        Vector2.down,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.down,
                        Vector2.zero,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.left, 
                        // Power pellet active
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.up,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.up
                    });

                    ghostManager.Ghosts[2].SetPredefinedPath(new Vector2[]{ // Inky
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.right,
                        // Pellet starts
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        // Caught
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.up,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.left
                    });

                    ghostManager.Ghosts[3].SetPredefinedPath(new Vector2[]{ // Clyde
                        Vector2.down,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.down,
                        Vector2.zero,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.up,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.right
                    });

                    pacMan.SetPredefinedPath(new Vector2[]{
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.zero,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.up,
                        Vector2.zero,
                        Vector2.zero,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.left,
                        Vector2.down,
                        Vector2.zero,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.zero,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.right,
                        Vector2.up,
                        Vector2.right,
                        Vector2.zero,
                        Vector2.down,
                        Vector2.left,
                        Vector2.down,
                        Vector2.right,
                        Vector2.down,
                        Vector2.left,
                        Vector2.zero,
                        Vector2.zero,
                        Vector2.up,
                        Vector2.right,
                        Vector2.up,
                        Vector2.left,
                        Vector2.up,
                        Vector2.right,
                    });

                    break;
                case 3:
                    SetPelletsActive(true);
                    pelletManager.RestoreAllPellets();
                    statusDisplay.SetGameOverTextVisible(true);
                    break;
                case 4:
                    // Show pacman, show ghosts
                    SetPacManActive(true);
                    SetGhostsActive(true);
                    break;
                case 5:
                    // Unfreeze
                    SetFrozen(false);
                    break;
                    // case 6:
                    //     if (!hasTimeSequenceQueued)
                    //     {
                    //         StartTimeSequence(PacManTimeSequence.AttractScreenWaitForStart);
                    //     }
                    //     break;
            }
        }

        private void TimeSequenceStepWaitForStart(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    SetGameState(PacManGameState.WaitForStart);
                    HideEverything();
                    soundManager.SuppressSound(false);
                    soundManager.PlayCoinSound();
                    break;
                case 1:
                    SetPressStartButtonScreenVisible(true);
                    if (playerInput.active == false && hasTimeSequenceQueued == false)
                    {
                        StartTimeSequence(PacManTimeSequence.WaitForStartTimeout);
                    }
                    break;                    
            }
        }

        private void TimeSequenceStepWaitForStartTimeout(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    break;
                case 1:
                    if (playerInput.active == false && hasTimeSequenceQueued == false)
                    {
                        StartAttractMode();
                    }
                    break;
            }
        }

        private void TimeSequenceStepStartNewGame(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    // Prepare new game, hide everything except score bar
                    gameState = PacManGameState.InGame;

                    HideEverything();

                    SetMazeActive(true);

                    InitializeNewGame();
                    InitializeLevel();
                    RestartLevel();
                    SetFrozen(true);
                    soundManager.SuppressSound(false);
                    soundManager.PlayGameStartSound();
                    break;
                case 1:
                    // Show maze, lives indicator, level indicator, player 1 and ready text

                    // SOMEWHERE IN HERE UNITY (EDITOR) APPEARS TO HAVE A SMALL RANDOM CHANCE OF CRASHING !!
                    Debug.Log("Log dump in case of crash");
                    Debug.Log("Setting pellets visible");
                    SetPelletsActive(true);
                    Debug.Log("Setting maze visible");
                    SetMazeVisible(true);
                    Debug.Log("Setting extra lives display visible");
                    statusDisplay.SetExtraLivesDisplayVisible(true);
                    Debug.Log("Setting level display visible");
                    statusDisplay.SetLevelDisplayVisible(true);
                    Debug.Log("Setting player 1 text visible");
                    statusDisplay.SetPlayer1TextVisible(true);
                    Debug.Log("Setting ready text visible");
                    statusDisplay.SetReadyTextVisible(true);
                    Debug.Log("Starting 1UP blink");
                    statusDisplay.SetLabel1UPTextBlinking(true);
                    break;
                case 2:
                    // Subtract a life
                    DecrementLives();
                    break;
                case 3:
                    // Remove Player 1 text
                    statusDisplay.SetPlayer1TextVisible(false);
                    break;
                case 4:
                    // Show ghosts and pacman
                    SetGhostsActive(true);
                    SetPacManActive(true);
                    break;
                case 5:
                    // Remove ready text
                    statusDisplay.SetReadyTextVisible(false);
                    break;
                case 6:
                    // Start game, end sequence
                    soundManager.StartGhostSound();
                    SetFrozen(false);
                    break;
            }
        }

        private void TimeSequenceStepBoardClear(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    // Freeze
                    SetFrozen(true);
                    soundManager.StopAllSound();
                    break;
                case 1:
                    // Start board blinking, hide pellets in case of rack test
                    BoardClearAnimation();
                    SetPelletsActive(false);
                    break;
                case 2:
                    // Hide ghosts
                    SetGhostsActive(false);
                    break;
                case 3:
                    // Hide maze, lives indicator, level indicator
                    SetMazeVisible(false);
                    statusDisplay.SetExtraLivesDisplayVisible(false);
                    statusDisplay.SetLevelDisplayVisible(false);
                    break;
                case 4:
                    // Hide score bar
                    statusDisplay.SetScoreDisplayVisible(false);
                    break;
                case 5:
                    // Hide pacman, show level indicator with old level
                    SetPacManActive(false);
                    statusDisplay.SetLevelDisplayVisible(true);
                    break;
                case 6:
                    // Call handler for what should happen next
                    InsertTimeSequence(PacManTimeSequence.StartNewLevel);
                    break;
            }
        }

        private void TimeSequenceStepStartNewLevel(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    // Reset, show maze and score display
                    InitializeLevel();
                    RestartLevel();
                    SetMazeVisible(true);
                    statusDisplay.SetScoreDisplayVisible(true);
                    soundManager.SuppressSound(false);
                    break;
                case 1:
                    // Increment level, show ready, show pellets, show lives indicators
                    IncrementLevel();
                    statusDisplay.SetExtraLivesDisplayVisible(true);
                    statusDisplay.SetReadyTextVisible(true);
                    SetPelletsActive(true);
                    break;
                case 2:
                    // Show pacman, show ghosts
                    SetPacManActive(true);
                    SetGhostsActive(true);
                    break;
                case 3:
                    // Hide ready
                    statusDisplay.SetReadyTextVisible(false);
                    break;
                case 4:
                    // Unfreeze
                    SetFrozen(false);
                    soundManager.StartGhostSound();
                    break;
            }
        }

        private void TimeSequenceStepGhostCaught(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    // Freeze and hide pacman, but let ghosts already in the caught animation continue
                    SetFrozen(true, ghostIgnoreIfCaught: true);
                    SetPacManActive(false);
                    soundManager.PlayGhostEatSound();
                    break;
                case 1:
                    // Unfreeze and reveal pacman
                    SetPacManActive(true);
                    SetFrozen(false);
                    ghostManager.GhostCaughtContinue();
                    soundManager.SetGhostRetreat(true);
                    break;
            }
        }

        private void TimeSequenceStepPacManCaught(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    // Freeze (except for the ghost animations)
                    SetFrozen(true, ghostKeepAnimating: true);
                    soundManager.StopAllSound();
                    break;
                case 1:
                    // Hide ghosts, start pacman death animation
                    SetGhostsActive(false);
                    pacMan.SetDead(true);
                    break;
                case 2:
                    // Start playing death sound
                    soundManager.PlayDeathSound();
                    break;
                case 3:
                    // Hide pacman, start next state
                    SetPacManActive(false);
                    if (gameState == PacManGameState.AttractModeDemo)
                    {
                        break;
                    }
                    if (extraLives > 0)
                    {
                        InsertTimeSequence(PacManTimeSequence.RestartLevel);
                        break;
                    }

                    InsertTimeSequence(PacManTimeSequence.GameOver);
                    break;
                case 4:
                    if (gameState == PacManGameState.AttractModeDemo)
                    {
                        #if RECORDING_DEMO
                        // recorder.gameObject.SetActive(false);
                        #endif
                        InsertTimeSequence(PacManTimeSequence.AttractScreenIntroduction);
                    }
                    break;
            }
        }

        private void TimeSequenceStepRestartLevel(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    // Hide playfield and pellets
                    SetMazeVisible(false);
                    SetPelletsActive(false);
                    break;
                case 1:
                    // Make maze visible
                    RestartLevel();
                    SetMazeVisible(true);
                    break;
                case 2:
                    // Take life, show ready, show pellets
                    DecrementLives();
                    statusDisplay.SetReadyTextVisible(true);
                    SetPelletsActive(true);
                    break;
                case 3:
                    // Show pacman, show ghosts
                    SetPacManActive(true);
                    SetGhostsActive(true);
                    break;
                case 4:
                    // Hide ready
                    statusDisplay.SetReadyTextVisible(false);
                    break;
                case 5:
                    // Unfreeze
                    SetFrozen(false);
                    soundManager.SuppressSound(false);
                    soundManager.StartGhostSound();
                    soundManager.UpdatePelletCount(pelletCountRemaining);
                    break;
            }
        }

        private void TimeSequenceStepGameOver(int sequenceProgress)
        {
            switch (sequenceProgress)
            {
                case 0:
                    // Show game over text, freeze power pellet blink
                    statusDisplay.SetGameOverTextVisible(true);
                    pelletManager.FreezePowerPelletsBlink(true);
                    break;
                case 1:
                    // Stop text blinking, transition to attract screen
                    statusDisplay.SetLabel1UPTextBlinking(false);
                    StartAttractMode();
                    break;
            }
        }


        public int TimeSequenceProgress
        {
            get => timeSequenceProgress;
        }

        public float TimeSequenceSecondsPassed
        {
            get => timeSequenceSecondsPassed;
            set => TimeSequenceProgressToTime(value);
        }
#endregion
    }
}