/// <summary>
        /// Generate obstacles commands given a song and difficulty
        /// </summary>
        /// <param name="song">Beatmap data for a song and difficulty</param>
        /// <param name="difficultyName">Minecraft safe difficulty name</param>
        /// <param name="commandBasePath">Base folder path to generate new mcfunctions</param>
        /// <param name="packInfo">Beat Saber Parsed info</param>
        /// <param name="dpd">Data used for datapack generation</param>
        public static void GenerateObstacles(MapData song, string difficultyName, string commandBasePath, Info packInfo, DataPackData dpd)
        {
            Obstacle[] obstacles               = song.Obstacles;
            int        obstacleIndex           = 0;
            int        currentLevel            = 1;
            int        currentTick             = 0;
            int        maxTick                 = 0;
            int        minTick                 = -1;
            int        currentNumberOfCommands = 0;
            int        currentCommandLimit     = Globals.COMMAND_LIMIT;

            // Main note generation
            while (obstacleIndex < obstacles.Length)
            {
                string        commandLevelName     = difficultyName + Globals.LEVEL_OBSTACLE_NAME + currentLevel;
                string        commandLevelFileName = commandLevelName + Globals.MCFUNCTION;
                string        commandLevelFilePath = Path.Combine(dpd.folder_uuidFunctionsPath, commandLevelFileName);
                StringBuilder currentCommands      = new StringBuilder();
                int           maxNewTick           = 0;
                int           minNewTick           = 0;
                minTick = -1;

                // Continue to generate commands until all nodes and obstacles have been iterated though
                while (obstacleIndex < obstacles.Length && currentNumberOfCommands < currentCommandLimit)
                {
                    ObstacleDataToCommands(obstacles[obstacleIndex], packInfo.BeatsPerMinute, dpd.metersPerTick, ref currentCommands, ref currentNumberOfCommands, ref minNewTick, ref maxNewTick);
                    if (minTick == -1)
                    {
                        minTick = minNewTick;
                    }
                    maxTick = Mathf.Max(maxTick, maxNewTick);
                    obstacleIndex++;
                }

                if (obstacleIndex >= obstacles.Length)
                {
                    maxTick += (int)(dpd.ticksStartOffset + 1);
                    currentCommands.AppendFormat(Globals.templateStrings._finishedObstacles, maxTick);
                    if (minTick == -1)
                    {
                        minTick = maxTick - 1;
                    }
                }

                SafeFileManagement.SetFileContents(commandLevelFilePath, currentCommands.ToString());
                string baseCommand = string.Format(Globals.templateStrings._baseCommand, minTick, maxTick, dpd.folder_uuid, commandLevelName);
                SafeFileManagement.AppendFile(commandBasePath, baseCommand);
                currentCommandLimit = currentNumberOfCommands + Globals.COMMAND_LIMIT;
                minTick             = 0;
                maxTick             = 0;
                currentLevel++;
            }

            if (obstacles.Length == 0)
            {
                string        commandLevelName     = difficultyName + Globals.LEVEL_OBSTACLE_NAME + currentLevel;
                string        commandLevelFileName = commandLevelName + Globals.MCFUNCTION;
                string        commandLevelFilePath = Path.Combine(dpd.folder_uuidFunctionsPath, commandLevelFileName);
                StringBuilder currentCommands      = new StringBuilder();
                currentTick++;
                currentCommands.AppendFormat(Globals.templateStrings._finishedObstacles, currentTick);
                SafeFileManagement.SetFileContents(commandLevelFilePath, currentCommands.ToString());
                string baseCommand = string.Format(Globals.templateStrings._baseCommand,
                                                   minTick,
                                                   maxTick + (int)(dpd.ticksStartOffset + 1),
                                                   dpd.folder_uuid,
                                                   commandLevelName);
                SafeFileManagement.AppendFile(commandBasePath, baseCommand);
            }
        }
        /// <summary>
        /// Generate a minecraft datapack from Beat Saber data
        /// </summary>
        /// <param name="unzippedFolderPath">Path of unzipped Beat Saber data</param>
        /// <param name="datapackOutputPath">Path to output datapack</param>
        /// <param name="packInfo">Beat Saber Parsed info</param>
        /// <param name="beatMapSongList">List of Beat Saber song data</param>
        /// <param name="cancellationToken">Token that allows async function to be canceled</param>
        /// <returns></returns>
        public static async Task <ConversionError> FromBeatSaberData(string datapackOutputPath, BeatSaberMap beatSaberMap, IProgress <ConversionProgress> progress, CancellationToken cancellationToken)
        {
            return(await Task.Run(() =>
            {
                var unzippedFolderPath = beatSaberMap.ExtractedFilePath;
                if (!Directory.Exists(unzippedFolderPath))
                {
                    return Task.FromResult(ConversionError.UnzipError);
                }
                DataPackData dataPackData = new DataPackData(unzippedFolderPath, datapackOutputPath, beatSaberMap);
                if (beatSaberMap.InfoData.DifficultyBeatmapSets.Length == 0)
                {
                    return Task.FromResult(ConversionError.NoMapData);
                }
                // Copying Template
                string copiedTemplatePath = Path.Combine(unzippedFolderPath, Globals.TEMPLATE_DATA_PACK_NAME);
                if (!SafeFileManagement.DirectoryCopy(Globals.pathOfDatapackTemplate, unzippedFolderPath, true, Globals.excludeExtensions, Globals.NUMBER_OF_IO_RETRY_ATTEMPTS))
                {
                    return Task.FromResult(ConversionError.FailedToCopyFile);
                }
                try
                {
                    if (SafeFileManagement.MoveDirectory(copiedTemplatePath, dataPackData.datapackRootPath, Globals.NUMBER_OF_IO_RETRY_ATTEMPTS))
                    {
                        cancellationToken.ThrowIfCancellationRequested();

                        // Must change the folder names before searching for keys
                        string songname_uuidFolder = Path.Combine(dataPackData.datapackRootPath, Globals.DATA, Globals.FOLDER_UUID);
                        string newPath = Path.Combine(dataPackData.datapackRootPath, Globals.DATA, dataPackData.folder_uuid);
                        SafeFileManagement.MoveDirectory(songname_uuidFolder, newPath, Globals.NUMBER_OF_IO_RETRY_ATTEMPTS);

                        // Updating Copied files
                        Filemanagement.UpdateAllCopiedFiles(dataPackData.datapackRootPath, dataPackData.keyVars, true, Globals.excludeKeyVarExtensions);

                        // Copying Image Icon
                        string mapIcon = Path.Combine(unzippedFolderPath, beatSaberMap.InfoData.CoverImageFilename);
                        string packIcon = Path.Combine(dataPackData.datapackRootPath, Globals.PACK_ICON);
                        SafeFileManagement.CopyFileTo(mapIcon, packIcon, true, Globals.NUMBER_OF_IO_RETRY_ATTEMPTS);

                        cancellationToken.ThrowIfCancellationRequested();

                        progress.Report(new ConversionProgress(0.4f, "Generating main datapack files"));
                        var mcBeatDataError = GenerateMCBeatData(beatSaberMap, dataPackData, progress, cancellationToken);
                        if (mcBeatDataError != ConversionError.None)
                        {
                            return Task.FromResult(mcBeatDataError);
                        }
                        cancellationToken.ThrowIfCancellationRequested();

                        progress.Report(new ConversionProgress(0.9f, "Zipping files"));
                        Archive.Compress(dataPackData.datapackRootPath, dataPackData.fullOutputPath, true);
                        return Task.FromResult(ConversionError.None);
                    }
                }
                catch (OperationCanceledException wasCanceled)
                {
                    throw wasCanceled;
                }
                catch (ObjectDisposedException wasAreadyCanceled)
                {
                    throw wasAreadyCanceled;
                }
                return Task.FromResult(ConversionError.OtherFail);
            }));
        }
        /// <summary>
        /// Generate note commands given a song and difficulty
        /// </summary>
        /// <param name="song">Beatmap data for a song and difficulty</param>
        /// <param name="difficultyName">Minecraft safe difficulty name</param>
        /// <param name="commandBasePath">Base folder path to generate new mcfunctions</param>
        /// <param name="packInfo">Beat Saber Parsed info</param>
        /// <param name="dpd">Data used for datapack generation</param>
        public static void GenerateNotes(MapData song, string difficultyName, string commandBasePath, Info packInfo, DataPackData dpd)
        {
            double prevNodeTime            = 0;
            int    nodeRowID               = 1;
            int    currentLevel            = 1;
            int    currentTick             = 0;
            int    prevCurrentTick         = 0;
            int    currentNumberOfCommands = 0;
            int    noteIndex               = 0;
            int    currentCommandLimit     = Globals.COMMAND_LIMIT;

            var notes = song.Notes;

            // Main note generation
            while (noteIndex < notes.Length)
            {
                string        commandLevelName     = difficultyName + Globals.LEVEL_NOTE_NAME + currentLevel;
                string        commandLevelFileName = commandLevelName + Globals.MCFUNCTION;
                string        commandLevelFilePath = Path.Combine(dpd.folder_uuidFunctionsPath, commandLevelFileName);
                StringBuilder currentCommands      = new StringBuilder();

                // Continue to generate commands until all nodes and obstacles have been iterated though
                while (noteIndex < notes.Length && currentNumberOfCommands < currentCommandLimit)
                {
                    if (prevNodeTime != notes[noteIndex].Time)
                    {
                        nodeRowID++;
                    }

                    NodeDataToCommands(notes[noteIndex], packInfo.BeatsPerMinute, dpd.metersPerTick, nodeRowID, ref currentCommands, ref currentTick);

                    prevNodeTime             = notes[noteIndex].Time;
                    currentNumberOfCommands += 3;
                    noteIndex++;
                }

                if (noteIndex >= notes.Length)
                {
                    currentTick += (int)(dpd.ticksStartOffset + 1);;
                    currentCommands.AppendFormat(Globals.templateStrings._finishedNotes, currentTick);
                }

                SafeFileManagement.SetFileContents(commandLevelFilePath, currentCommands.ToString());
                string baseCommand = string.Format(Globals.templateStrings._baseCommand,
                                                   prevCurrentTick,
                                                   currentTick,
                                                   dpd.folder_uuid,
                                                   commandLevelName);
                SafeFileManagement.AppendFile(commandBasePath, baseCommand);
                prevCurrentTick     = currentTick + 1;
                currentCommandLimit = currentNumberOfCommands + Globals.COMMAND_LIMIT;
                currentLevel++;
            }

            // Note pretty buy create a command if no obstacles are present in a map
            if (notes.Length == 0)
            {
                currentTick += (int)(dpd.ticksStartOffset + 1);;
                string        commandLevelName     = difficultyName + Globals.LEVEL_NOTE_NAME + currentLevel;
                string        commandLevelFileName = commandLevelName + Globals.MCFUNCTION;
                string        commandLevelFilePath = Path.Combine(dpd.folder_uuidFunctionsPath, commandLevelFileName);
                StringBuilder currentCommands      = new StringBuilder();
                SafeFileManagement.SetFileContents(commandLevelFilePath, currentCommands.ToString());
                string baseCommand = string.Format(Globals.templateStrings._baseCommand,
                                                   prevCurrentTick,
                                                   currentTick,
                                                   dpd.folder_uuid,
                                                   commandLevelName);
                SafeFileManagement.AppendFile(commandBasePath, baseCommand);
                currentCommands.AppendFormat(Globals.templateStrings._finishedNotes, currentTick);
            }
        }
        public static int EventDataToCommands(BeatSaber.Data.Event bEvent, float bpm, DataPackData dpd, ref StringBuilder commandList, ref int wholeTick, ref List <EventFadeSave> autoOffTick)
        {
            int    valueType = bEvent.Value;
            string eventName;
            int    typeType = bEvent.Type;
            int    indexValue;
            int    addedLines = 0;

            switch (typeType)
            {
            case 0:
                eventName  = "BackLasersGroup";
                indexValue = 0;
                break;

            case 1:
                eventName  = "RingLightsGroup";
                indexValue = 1;
                break;

            case 2:
                eventName  = "LeftRotatingLasersGroup";
                indexValue = 2;
                break;

            case 3:
                eventName  = "RightRotatingLasersGroup";
                indexValue = 3;
                break;

            case 4:
                eventName  = "CenterLightsGroup";
                indexValue = 4;
                break;

            case 5:
                eventName  = "BoostLight";
                indexValue = 5;
                break;

            case 6:
                eventName  = "ExtraLeftSideLights";
                indexValue = 6;
                break;

            case 7:
                eventName  = "ExtraRightSideLights";
                indexValue = 7;
                break;

            case 10:
                eventName  = "LeftSideLasers";
                indexValue = 8;
                break;

            case 11:
                eventName  = "RightSideLasers";
                indexValue = 9;
                break;

            default:
                return(addedLines);
            }

            double beatsPerTick = bpm / 60.0d / 20;
            double exactTick    = bEvent.Time / beatsPerTick;

            wholeTick = (int)Mathf.Floor((float)exactTick) + (int)dpd.ticksStartOffset;

            for (int i = 0; i < autoOffTick.Count; i++)
            {
                if (autoOffTick[i].bEvent != null && autoOffTick[i].bEvent.Type != typeType && autoOffTick[i].tickCount >= 0 && autoOffTick[i].tickCount <= wholeTick)
                {
                    commandList.AppendFormat(Globals.templateStrings._eventOnOff, autoOffTick[i].tickCount, autoOffTick[i].eventName + "OnOff", 0);
                    commandList.AppendFormat(Globals.templateStrings._eventColor, autoOffTick[i].tickCount, autoOffTick[i].eventName + "Color", 0);
                    addedLines += 2;
                }
            }
            if ((valueType == 3 || valueType == 7))
            {
                autoOffTick[indexValue].tickCount = wholeTick + 40;
                autoOffTick[indexValue].bEvent    = bEvent;
                autoOffTick[indexValue].eventName = eventName;
            }
            else
            {
                autoOffTick[indexValue].tickCount = -1;
                autoOffTick[indexValue].bEvent    = null;
                autoOffTick[indexValue].eventName = "";
            }

            bool isLightOn  = valueType != 0;
            int  lightColor = (valueType > 0 && valueType <= 4) ? 16 : 24;

            commandList.AppendFormat(Globals.templateStrings._eventOnOff, wholeTick, eventName + "OnOff", isLightOn ? 1 : 0);
            commandList.AppendFormat(Globals.templateStrings._eventColor, wholeTick, eventName + "Color", lightColor);
            return(addedLines + 2);
        }
        public static void GenerateEvents(MapData song, string difficultyName, string commandBasePath, Info packInfo, DataPackData dpd)
        {
            int currentLevel            = 1;
            int currentTick             = 0;
            int prevCurrentTick         = 0;
            int currentNumberOfCommands = 0;
            int noteIndex           = 0;
            int currentCommandLimit = Globals.COMMAND_LIMIT;

            var bEvents = song.Events.Where(x => x.Value >= 0 && x.Value <= 11).ToArray();

            List <EventFadeSave> autoOffTick = new List <EventFadeSave>();

            for (int i = 0; i < 10; i++)
            {
                autoOffTick.Add(new EventFadeSave());
            }
            // Main note generation
            while (noteIndex < bEvents.Length)
            {
                string        commandLevelName     = difficultyName + Globals.LEVEL_EVENT_NAME + currentLevel;
                string        commandLevelFileName = commandLevelName + Globals.MCFUNCTION;
                string        commandLevelFilePath = Path.Combine(dpd.folder_uuidFunctionsPath, commandLevelFileName);
                StringBuilder currentCommands      = new StringBuilder();

                // Continue to generate commands until all events
                while (noteIndex < bEvents.Length && currentNumberOfCommands < currentCommandLimit)
                {
                    currentNumberOfCommands += EventDataToCommands(bEvents[noteIndex], packInfo.BeatsPerMinute, dpd, ref currentCommands, ref currentTick, ref autoOffTick);
                    noteIndex++;
                }

                if (noteIndex >= bEvents.Length)
                {
                    currentTick += (int)(dpd.ticksStartOffset + 1);;
                    currentCommands.AppendFormat(Globals.templateStrings._finishedEvents, currentTick);
                }

                SafeFileManagement.SetFileContents(commandLevelFilePath, currentCommands.ToString());
                string baseCommand = string.Format(Globals.templateStrings._baseCommand,
                                                   prevCurrentTick,
                                                   currentTick,
                                                   dpd.folder_uuid,
                                                   commandLevelName);
                SafeFileManagement.AppendFile(commandBasePath, baseCommand);
                prevCurrentTick     = currentTick + 1;
                currentCommandLimit = currentNumberOfCommands + Globals.COMMAND_LIMIT;
                currentLevel++;
            }


            // Note pretty buy create a command if no obstacles are present in a map
            if (bEvents.Length == 0)
            {
                currentTick += (int)(dpd.ticksStartOffset + 1);;
                string        commandLevelName     = difficultyName + Globals.LEVEL_EVENT_NAME + currentLevel;
                string        commandLevelFileName = commandLevelName + Globals.MCFUNCTION;
                string        commandLevelFilePath = Path.Combine(dpd.folder_uuidFunctionsPath, commandLevelFileName);
                StringBuilder currentCommands      = new StringBuilder();
                SafeFileManagement.SetFileContents(commandLevelFilePath, currentCommands.ToString());
                string baseCommand = string.Format(Globals.templateStrings._baseCommand,
                                                   prevCurrentTick,
                                                   currentTick,
                                                   dpd.folder_uuid,
                                                   commandLevelName);
                SafeFileManagement.AppendFile(commandBasePath, baseCommand);
                currentCommands.AppendFormat(Globals.templateStrings._finishedNotes, currentTick);
            }
        }
        /// <summary>
        /// Main generation of minecraft commands for beat saber data
        /// </summary>
        /// <param name="beatMapSongList">List of Beat Saber song data</param>
        /// <param name="packInfo">Beat Saber Parsed info</param>
        /// <param name="dpd">Data used for datapack generation</param>
        /// <returns></returns>
        public static ConversionError GenerateMCBeatData(BeatSaberMap beatSaberMap, DataPackData dpd, IProgress <ConversionProgress> progress, CancellationToken cancellationToken)
        {
            StringBuilder difficultyDisplayCommands = new StringBuilder();
            StringBuilder scoreboardCommands        = new StringBuilder();
            StringBuilder spawnOriginCommands       = new StringBuilder();
            StringBuilder spawnNotesBaseCommands    = new StringBuilder();
            int           difficultyNumber          = 1;

            // Iterate though each song difficulty
            var mapDataInfos      = beatSaberMap.MapDataInfos;
            var progressPercent   = 0.4f;
            var mapDataInfosKeys  = mapDataInfos.Keys.ToArray();
            var progressIncrament = mapDataInfosKeys.Length * 0.5f;

            for (int i = 0; i < mapDataInfosKeys.Length; i++)
            {
                var mapDataInfo = mapDataInfos[mapDataInfosKeys[i]];
                if (mapDataInfo.MapData == null || mapDataInfo.MapData.Notes == null || mapDataInfo.MapData.Obstacles == null)
                {
                    return(ConversionError.NoMapData);
                }
                progress.Report(new ConversionProgress(progressPercent + progressIncrament, "Generating minecraft commands"));
                if (mapDataInfo.MapData.Notes.Length > 0 || mapDataInfo.MapData.Obstacles.Length > 0)
                {
                    string difficultyName = mapDataInfo.DifficultyBeatmapInfo.Difficulty.MakeMinecraftSafe();

                    // Append running command lists
                    string songDifficultyID = dpd.songGuid + difficultyNumber.ToString();
                    scoreboardCommands.AppendFormat(Globals.templateStrings._scoreboardCommand, songDifficultyID);
                    spawnOriginCommands.AppendFormat(Globals.templateStrings._spawnOriginCommands, -dpd.metersPerTick * dpd.ticksStartOffset, difficultyNumber);
                    spawnNotesBaseCommands.AppendFormat(Globals.templateStrings._spawnNotesBaseCommand, difficultyNumber, dpd.folder_uuid, difficultyName);

                    CreateDifficultyDisplay(songDifficultyID, difficultyName, dpd.folder_uuid, ref difficultyDisplayCommands);

                    // Write difficulty-specific-file commands
                    string playCommands = string.Format(Globals.templateStrings._playCommands, difficultyNumber, dpd.folder_uuid);
                    string playPath     = Path.Combine(dpd.folder_uuidFunctionsPath, difficultyName + "_play" + Globals.MCFUNCTION);
                    SafeFileManagement.SetFileContents(playPath, playCommands);

                    string playSongCommand = string.Format(Globals.templateStrings._playSongCommand, dpd.ticksStartOffset - 1, dpd.folder_uuid);
                    string commandBasePath = Path.Combine(dpd.folder_uuidFunctionsPath, difficultyName + Globals.MCFUNCTION);
                    SafeFileManagement.AppendFile(commandBasePath, playSongCommand);

                    string completedSongCommand = string.Format(Globals.templateStrings._completedSong, difficultyNumber, songDifficultyID);
                    string completedSongPath    = Path.Combine(dpd.folder_uuidFunctionsPath, Globals.MAP_DIFFICULTY_COMPLETED);
                    SafeFileManagement.AppendFile(completedSongPath, completedSongCommand);

                    // Generate main note/obstacle data/light data
                    GenerateNotes(mapDataInfo.MapData, difficultyName, commandBasePath, beatSaberMap.InfoData, dpd);
                    GenerateObstacles(mapDataInfo.MapData, difficultyName, commandBasePath, beatSaberMap.InfoData, dpd);
                    GenerateEvents(mapDataInfo.MapData, difficultyName, commandBasePath, beatSaberMap.InfoData, dpd);

                    difficultyNumber++;
                }
            }

            // Write collected commands to files
            string difficultiesFunctionPath  = Path.Combine(dpd.folder_uuidFunctionsPath, Globals.DIFFICULTIES);
            string initFunctionPath          = Path.Combine(dpd.folder_uuidFunctionsPath, Globals.INIT_FUNCTION);
            string setSpawnOrginFunctionPath = Path.Combine(dpd.folder_uuidFunctionsPath, Globals.SET_SPAWN_ORIGIN);

            SafeFileManagement.AppendFile(dpd.spawnNotesBasePath, spawnNotesBaseCommands.ToString());
            SafeFileManagement.AppendFile(setSpawnOrginFunctionPath, spawnOriginCommands.ToString());
            SafeFileManagement.AppendFile(initFunctionPath, scoreboardCommands.ToString());

            // Add back button in tellraw
            difficultyDisplayCommands.Append(Globals.templateStrings._mainMenuBack);
            SafeFileManagement.AppendFile(difficultiesFunctionPath, difficultyDisplayCommands.ToString());
            return(ConversionError.None);
        }