Editor/InkCompileTool.cs
using Editor;
using Ink;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

public static class InkCompileTool
{
    private static FileSystemWatcher _watcher = null;

    // queue for ink files to compile
    private static List<string> inkFilesToCompile = new();

    [Menu("Editor", "Ink/Compile All Ink Files")]
    [Shortcut("EditorTool.force_compile_ink_files", "ctrl+shift+i")]
    public static void CompileAllInkFiles()
    {
        try
        {
            string pathToWatch = FileSystem.Mounted.GetFullPath("ink");
            string[] files = Directory.GetFiles(pathToWatch, "*.ink", SearchOption.AllDirectories);

            foreach (string file in files)
            {
                CompileInk(file);
            }
        }
        catch (Exception ex)
        {
            Log.Error($"Error compiling all ink files: {ex.Message}");
        }

        inkFilesToCompile.Clear();
        StartWatching();

    }


    [EditorEvent.Hotload]
    public static void OnHotload()
    {
        CompileAllInkFiles();
    }

    [EditorEvent.Frame]
    public static void OnFrame()
    {
        if (Application.FocusWidget == null)
            return;

        // Log.Info($"Ink files to compile: {_inkFilesToCompile.Count}");
        // compile all ink in the queue
        for (int i = 0; i < inkFilesToCompile.Count; i++)
        {
            string path = inkFilesToCompile[i];
            Log.Info($"In the queue: {path}");
            if ( IsFileReady(path) )
            {
                CompileInk(path);
                inkFilesToCompile.RemoveAt(i);
            }
            else
            {
                Log.Warning($"File {path} is not ready yet");
                continue;
            }
        }

    }

    private static bool IsFileReady( string path )
    {
        try
        {
            using ( FileStream stream = File.Open( path, FileMode.Open, FileAccess.Read, FileShare.None ) )
            {
                return stream.Length > 0;
            }
        }
        catch ( IOException )
        {
            Log.Warning( $"File {path} is not ready yet" );
            return false;
        }
    }

    public static void CompileInk(string path_to_compile)
    {
        string filename = Path.GetFileName(path_to_compile);
        Stopwatch stopwatch = new();
        stopwatch.Start();
        Log.Info($"Started compiling {filename} at {DateTime.Now}");
        try
        {
            var ink_source = File.ReadAllText(path_to_compile);
            var compiler = new Compiler(ink_source, new Compiler.Options
            {
                countAllVisits = true,
                fileHandler = new SboxInkFileHandler(Path.GetDirectoryName(path_to_compile)),
                errorHandler = (string message, ErrorType type) =>
                {
                    Log.Warning($"Ink error: {message}");
                }
            });
            var compiled = compiler.Compile();
            var json = compiled.ToJson();
            // Log.Info(json);

            var json_path = path_to_compile + ".json";
            File.WriteAllText(json_path, json);
        }
        catch (Exception ex)
        {
            Log.Error($"Error compiling ink: {ex.Message}");
        }
        finally
        {
            stopwatch.Stop();
            Log.Info($"Finished compiling {path_to_compile} in {stopwatch.ElapsedMilliseconds} ms");
        }
    }

    [Menu("Editor", "Ink/Start watching for changes")]
    public static void StartWatching()
    {
        try
        {
            if ( _watcher != null )
            {
                Log.Info( "Watcher already exists, disposing..." );
                _watcher.Dispose();
            }

            string pathToWatch = FileSystem.Mounted.GetFullPath( "ink" );
            _watcher = new FileSystemWatcher
            {
                Path = pathToWatch,
                Filter = "*.ink", // Watch only files ending with .ink
                IncludeSubdirectories = true, // Include subdirectories
                NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName // Watch for changes in file content and name
            };

            _watcher.Changed += OnChanged;
            _watcher.Created += OnChanged;
            _watcher.Deleted += OnDeleted;

            _watcher.EnableRaisingEvents = true;

            Log.Info( $"Watching directory: {pathToWatch}" );
        }
        catch (Exception ex)
        {
            Log.Error($"Error starting watcher: {ex.Message}");
        }
    }


    private static void OnDeleted(object sender, FileSystemEventArgs e)
    {
        try
        {
            Log.Info($"File {e.ChangeType}: {e.FullPath}");
        }
        catch (Exception ex)
        {
            Log.Error($"Error handling file change: {ex.Message}");
        }
    }

    private static void OnChanged(object sender, FileSystemEventArgs e)
    {
        try
        {
            if (!inkFilesToCompile.Contains(e.FullPath)) {
                inkFilesToCompile.Add(e.FullPath);
                Log.Info($"Added {e.FullPath} to the compile queue ({e.ChangeType})");
            }
        }
        catch (Exception ex)
        {
            Log.Error($"Error handling file change: {ex.Message}");
        }
    }
}