/// <summary>
        /// Updates every localization .CSV file associated with this
        /// .yarnproject file.
        /// </summary>
        /// <remarks>
        /// This method updates each localization file by performing the
        /// following operations:
        ///
        /// - Inserts new entries if they're present in the base
        /// localization and not in the translated localization
        ///
        /// - Removes entries if they're present in the translated
        /// localization and not in the base localization
        ///
        /// - Detects if a line in the base localization has changed its
        /// Lock value from when the translated localization was created,
        /// and update its Comment
        /// </remarks>
        /// <param name="serializedObject">A serialized object that
        /// represents a <see cref="YarnProjectImporter"/>.</param>
        internal static void UpdateLocalizationCSVs(YarnProjectImporter yarnProjectImporter)
        {
            if (yarnProjectImporter.CanGenerateStringsTable == false)
            {
                Debug.LogError($"Can't update localization CSVs for Yarn Project \"{yarnProjectImporter.name}\" because not every line has a tag.");
                return;
            }

            var baseLocalizationStrings = yarnProjectImporter.GenerateStringsTable();

            var localizations = yarnProjectImporter.languagesToSourceAssets;

            var modifiedFiles = new List <TextAsset>();

            try
            {
                AssetDatabase.StartAssetEditing();

                foreach (var loc in localizations)
                {
                    var fileWasChanged = UpdateLocalizationFile(baseLocalizationStrings, loc.languageID, loc.stringsFile);

                    if (fileWasChanged)
                    {
                        modifiedFiles.Add(loc.stringsFile);
                    }
                }

                if (modifiedFiles.Count > 0)
                {
                    Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles.Select(f => AssetDatabase.GetAssetPath(f)))}");
                }
                else
                {
                    Debug.Log($"No files needed updating.");
                }
            }
            finally
            {
                AssetDatabase.StopAssetEditing();
            }
        }
        /// <summary>
        /// Writes a .csv file to disk at the path indicated by <paramref
        /// name="destination"/>, containing all of the lines found in the
        /// scripts referred to by <paramref name="yarnProjectImporter"/>.
        /// </summary>
        /// <remarks>
        /// The file generated is in a format ready to be added to the <see
        /// cref="YarnProjectImporter.languagesToSourceAssets"/> list.
        /// </remarks>
        /// <param name="yarnProjectImporter">The YarnProjectImporter to
        /// extract strings from.</param>
        /// <param name="destination">The path to write the file
        /// to.</param>
        /// <returns><see langword="true"/> if the file was written
        /// successfully, <see langword="false"/> otherwise.</returns>
        /// <exception cref="CsvHelper.CsvHelperException">Thrown when an
        /// error is encountered when generating the CSV data.</exception>
        /// <exception cref="IOException">Thrown when an error is
        /// encountered when writing the data to disk.</exception>
        internal static bool WriteStringsFile(string destination, YarnProjectImporter yarnProjectImporter)
        {
            // Perform a strings-only compilation to get a full strings
            // table, and generate the CSV.
            var stringTable = yarnProjectImporter.GenerateStringsTable();

            // If there was an error, bail out here
            if (stringTable == null)
            {
                return(false);
            }

            // Convert the string tables to CSV...
            var outputCSV = StringTableEntry.CreateCSV(stringTable);

            // ...and write it to disk.
            File.WriteAllText(destination, outputCSV);

            return(true);
        }
        internal static void AddLineTagsToFilesInYarnProject(YarnProjectImporter importer)
        {
            // First, gather all existing line tags, so that we don't
            // accidentally overwrite an existing one. Do this by finding
            // all yarn scripts in all yarn projects, and get the string
            // tags inside them.

            var allYarnFiles =
                // get all yarn projects across the entire project
                AssetDatabase.FindAssets($"t:{nameof(YarnProject)}")
                // Get the path for each asset's GUID
                .Select(guid => AssetDatabase.GUIDToAssetPath(guid))
                // Get the importer for each asset at this path
                .Select(path => AssetImporter.GetAtPath(path))
                // Ensure it's a YarnProjectImporter
                .OfType <YarnProjectImporter>()
                // Get all of their source scripts, as a single sequence
                .SelectMany(importerSelect => importer.sourceScripts)
                // Get the path for each asset
                .Select(sourceAsset => AssetDatabase.GetAssetPath(sourceAsset))
                // get each asset importer for that path
                .Select(path => AssetImporter.GetAtPath(path))
                // ensure that it's a YarnImporter
                .OfType <YarnImporter>()
                // get the path for each importer's asset (the compiler
                // will use this)
                .Select(importerSelect => AssetDatabase.GetAssetPath(importer))
                // remove any nulls, in case any are found
                .Where(path => path != null);

#if YARNSPINNER_DEBUG
            var stopwatch = System.Diagnostics.Stopwatch.StartNew();
#endif

            // Compile all of these, and get whatever existing string tags
            // they had. Do each in isolation so that we can continue even
            // if a file contains a parse error.
            var allExistingTags = allYarnFiles.SelectMany(path =>
            {
                try
                {
                    // Compile this script in strings-only mode to get
                    // string entries
                    var compilationJob             = Yarn.Compiler.CompilationJob.CreateFromFiles(path);
                    compilationJob.CompilationType = Yarn.Compiler.CompilationJob.Type.StringsOnly;

                    var result = Yarn.Compiler.Compiler.Compile(compilationJob);
                    return(result.StringTable.Where(i => i.Value.isImplicitTag == false).Select(i => i.Key));
                }
                catch (Yarn.Compiler.CompilerException e)
                {
                    Debug.LogWarning($"Can't check for existing line tags in {path}, because a compiler exception was thrown: {e}");
                    return(new string[] { });
                }
            }).ToList(); // immediately execute this query so we can determine timing information

#if YARNSPINNER_DEBUG
            stopwatch.Stop();
            Debug.Log($"Checked {allYarnFiles.Count()} yarn files for line tags in {stopwatch.ElapsedMilliseconds}ms");
#endif

            var modifiedFiles = new List <string>();

            try
            {
                AssetDatabase.StartAssetEditing();

                foreach (var script in importer.sourceScripts)
                {
                    var assetPath = AssetDatabase.GetAssetPath(script);
                    var contents  = File.ReadAllText(assetPath);

                    // Produce a version of this file that contains line
                    // tags added where they're needed.
                    var taggedVersion = Yarn.Compiler.Utility.AddTagsToLines(contents, allExistingTags);

                    // If this produced a modified version of the file,
                    // write it out and re-import it.
                    if (contents != taggedVersion)
                    {
                        modifiedFiles.Add(Path.GetFileNameWithoutExtension(assetPath));

                        File.WriteAllText(assetPath, taggedVersion, System.Text.Encoding.UTF8);
                        AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.Default);
                    }
                }
            }
            catch (System.Exception e) {
                Debug.LogError($"Encountered an error when updating scripts: {e}");
                return;
            }
            finally
            {
                AssetDatabase.StopAssetEditing();
            }

            // Report on the work we did.
            if (modifiedFiles.Count > 0)
            {
                Debug.Log($"Updated the following files: {string.Join(", ", modifiedFiles)}");
            }
            else
            {
                Debug.Log("No files needed updating.");
            }
        }
        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            EditorGUILayout.Space();

            if (sourceScriptsProperty.arraySize == 0)
            {
                EditorGUILayout.HelpBox("This Yarn Project has no content. Add Yarn Scripts to it.", MessageType.Warning);
            }
            EditorGUILayout.PropertyField(sourceScriptsProperty, true);

            EditorGUILayout.Space();

            if (string.IsNullOrEmpty(compileErrorProperty.stringValue) == false)
            {
                EditorGUILayout.HelpBox(compileErrorProperty.stringValue, MessageType.Error);
            }

            serializedDeclarationsList.DrawLayout();

            EditorGUILayout.PropertyField(defaultLanguageProperty, new GUIContent("Default Language"));

            CurrentProjectDefaultLanguageProperty = defaultLanguageProperty;

            EditorGUILayout.PropertyField(languagesToSourceAssetsProperty);

            CurrentProjectDefaultLanguageProperty = null;

            YarnProjectImporter yarnProjectImporter = serializedObject.targetObject as YarnProjectImporter;

            // Ask the project importer if it can generate a strings table.
            // This involves querying several assets, which means various
            // exceptions might get thrown, which we'll catch and log (if
            // we're in debug mode).
            bool canGenerateStringsTable;

            try
            {
                canGenerateStringsTable = yarnProjectImporter.CanGenerateStringsTable;
            }
            catch (System.Exception e)
            {
#if YARNSPINNER_DEBUG
                Debug.LogWarning($"Encountered in error when checking to see if Yarn Project Importer could generate a strings table: {e}", this);
#endif
                canGenerateStringsTable = false;
            }

            using (new EditorGUI.DisabledScope(canGenerateStringsTable == false))
            {
                if (GUILayout.Button("Export Strings as CSV"))
                {
                    var currentPath      = AssetDatabase.GetAssetPath(serializedObject.targetObject);
                    var currentFileName  = Path.GetFileNameWithoutExtension(currentPath);
                    var currentDirectory = Path.GetDirectoryName(currentPath);

                    var destinationPath = EditorUtility.SaveFilePanel("Export Strings CSV", currentDirectory, $"{currentFileName}.csv", "csv");

                    if (string.IsNullOrEmpty(destinationPath) == false)
                    {
                        // Generate the file on disk
                        YarnProjectUtility.WriteStringsFile(destinationPath, yarnProjectImporter);

                        // destinationPath may have been inside our Assets
                        // directory, so refresh the asset database
                        AssetDatabase.Refresh();
                    }
                }
                if (yarnProjectImporter.languagesToSourceAssets.Count > 0)
                {
                    if (GUILayout.Button("Update Existing Strings Files"))
                    {
                        YarnProjectUtility.UpdateLocalizationCSVs(yarnProjectImporter);
                    }
                }
            }

            // Does this project's source scripts list contain any actual
            // assets? (It can have a count of >0 and still have no assets
            // when, for example, you've just clicked the + button but
            // haven't dragged an asset in yet.)
            var hasAnyTextAssets = yarnProjectImporter.sourceScripts.Where(s => s != null).Count() > 0;

            using (new EditorGUI.DisabledScope(canGenerateStringsTable == true || hasAnyTextAssets == false))
            {
                if (GUILayout.Button("Add Line Tags to Scripts"))
                {
                    YarnProjectUtility.AddLineTagsToFilesInYarnProject(yarnProjectImporter);
                }
            }

            var hadChanges = serializedObject.ApplyModifiedProperties();

            if (hadChanges)
            {
                Debug.Log($"{nameof(YarnProjectImporterEditor)} had changes");
            }

#if UNITY_2018
            // Unity 2018's ApplyRevertGUI is buggy, and doesn't
            // automatically detect changes to the importer's
            // serializedObject. This means that we'd need to track the
            // state of the importer, and don't have a way to present a
            // Revert button.
            //
            // Rather than offer a broken experience, on Unity 2018 we
            // immediately reimport the changes. This is slow (we're
            // serializing and writing the asset to disk on every property
            // change!) but ensures that the writes are done.
            if (hadChanges)
            {
                // Manually perform the same tasks as the 'Apply' button
                // would
                ApplyAndImport();
            }
#endif

#if UNITY_2019_1_OR_NEWER
            // On Unity 2019 and newer, we can use an ApplyRevertGUI that
            // works identically to the built-in importer inspectors.
            ApplyRevertGUI();
#endif
        }