using System.Buffers;
using System.Diagnostics;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Audio;
using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.TTS.RVC;

public class RVCFilter : IAudioFilter, IDisposable
{
    private const int ProcessingSampleRate = 16000;

    private const int OutputSampleRate = 32000;

    private const int FinalSampleRate = 24000;

    private const int MaxInputDuration = 30; // seconds

    private readonly SemaphoreSlim _initLock = new(1, 1);

    private readonly ILogger<RVCFilter> _logger;

    private readonly IModelProvider _modelProvider;

    private readonly IDisposable? _optionsChangeRegistration;

    private readonly IOptionsMonitor<RVCFilterOptions> _optionsMonitor;

    private readonly IRVCVoiceProvider _rvcVoiceProvider;

    private RVCFilterOptions _currentOptions;

    private bool _disposed;

    private IF0Predictor? _f0Predictor;

    private OnnxRVC? _rvcModel;

    public RVCFilter(
        IOptionsMonitor<RVCFilterOptions> optionsMonitor,
        IModelProvider                    modelProvider,
        IRVCVoiceProvider                 rvcVoiceProvider,
        ILogger<RVCFilter>                logger)
    {
        _optionsMonitor   = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
        _modelProvider    = modelProvider;
        _rvcVoiceProvider = rvcVoiceProvider;
        _logger           = logger ?? throw new ArgumentNullException(nameof(logger));
        _currentOptions   = optionsMonitor.CurrentValue;

        _ = InitializeAsync(_currentOptions);

        // Register for options changes
        _optionsChangeRegistration = _optionsMonitor.OnChange(OnOptionsChanged);
    }

    public void Process(AudioSegment audioSegment)
    {
        if ( _disposed )
        {
            throw new ObjectDisposedException(nameof(RVCFilter));
        }

        if ( _rvcModel == null || _f0Predictor == null ||
             audioSegment?.AudioData == null || audioSegment.AudioData.Length == 0 )
        {
            return;
        }

        // Get the latest options for processing
        var options            = _currentOptions;
        var originalSampleRate = audioSegment.SampleRate;

        if ( !options.Enabled )
        {
            return;
        }

        // Start timing
        var stopwatch = Stopwatch.StartNew();

        // Step 1: Resample input to processing sample rate
        var resampleRatioToProcessing = (int)Math.Ceiling((double)ProcessingSampleRate / originalSampleRate);
        var resampledInputSize        = audioSegment.AudioData.Length * resampleRatioToProcessing;

        var resampledInput = ArrayPool<float>.Shared.Rent(resampledInputSize);
        try
        {
            var inputSampleCount = AudioConverter.ResampleFloat(
                                                                audioSegment.AudioData,
                                                                resampledInput,
                                                                1,
                                                                (uint)originalSampleRate,
                                                                ProcessingSampleRate);

            // Step 2: Process with RVC model
            var maxInputSamples  = OutputSampleRate * MaxInputDuration;
            var outputBufferSize = maxInputSamples + 2 * options.HopSize;

            var processingBuffer = ArrayPool<float>.Shared.Rent(outputBufferSize);
            try
            {
                var processedSampleCount = _rvcModel.ProcessAudio(
                                                                  resampledInput.AsMemory(0, inputSampleCount),
                                                                  processingBuffer,
                                                                  _f0Predictor,
                                                                  options.SpeakerId,
                                                                  options.F0UpKey);

                // Step 3: Resample to original sample rate
                var resampleRatioToOutput = (int)Math.Ceiling((double)originalSampleRate / OutputSampleRate);
                var finalOutputSize       = processedSampleCount * resampleRatioToOutput;

                var resampledOutput = ArrayPool<float>.Shared.Rent(finalOutputSize);
                try
                {
                    var finalSampleCount = AudioConverter.ResampleFloat(
                                                                        processingBuffer.AsMemory(0, processedSampleCount),
                                                                        resampledOutput,
                                                                        1,
                                                                        OutputSampleRate,
                                                                        (uint)originalSampleRate);

                    // Need one allocation for the final output buffer since AudioSegment keeps this reference
                    var finalBuffer = new float[finalSampleCount];
                    Array.Copy(resampledOutput, finalBuffer, finalSampleCount);

                    audioSegment.AudioData  = finalBuffer.AsMemory();
                    audioSegment.SampleRate = FinalSampleRate;
                }
                finally
                {
                    ArrayPool<float>.Shared.Return(resampledOutput);
                }
            }
            finally
            {
                ArrayPool<float>.Shared.Return(processingBuffer);
            }
        }
        finally
        {
            ArrayPool<float>.Shared.Return(resampledInput);
        }

        // Stop timing after processing is complete
        stopwatch.Stop();
        var processingTime = stopwatch.Elapsed.TotalSeconds;

        // Calculate final audio duration (based on the processed audio)
        var finalAudioDuration = audioSegment.AudioData.Length / (double)FinalSampleRate;

        // Calculate real-time factor
        var realTimeFactor = finalAudioDuration / processingTime;

        // Log the results using ILogger
        _logger.LogInformation("Generated {AudioDuration:F2}s audio in {ProcessingTime:F2}s (x{RealTimeFactor:F2} real-time)",
                               finalAudioDuration, processingTime, realTimeFactor);
    }

    public int Priority => 100;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private async void OnOptionsChanged(RVCFilterOptions newOptions)
    {
        if ( _disposed )
        {
            return;
        }

        if ( ShouldReinitialize(newOptions) )
        {
            DisposeResources();
            await InitializeAsync(newOptions);
        }

        _currentOptions = newOptions;
    }

    private bool ShouldReinitialize(RVCFilterOptions newOptions)
    {
        return _currentOptions.DefaultVoice != newOptions.DefaultVoice ||
               _currentOptions.HopSize != newOptions.HopSize;
    }

    private async ValueTask InitializeAsync(RVCFilterOptions options)
    {
        await _initLock.WaitAsync();
        try
        {
            var crepeModel  = await _modelProvider.GetModelAsync(Synthesis.ModelType.RVCCrepeTiny);
            var hubertModel = await _modelProvider.GetModelAsync(Synthesis.ModelType.RVCHubert);
            var rvcModel    = await _rvcVoiceProvider.GetVoiceAsync(options.DefaultVoice);

            // _f0Predictor = new CrepeOnnx(crepeModel.Path);
            _f0Predictor = new CrepeOnnxSimd(crepeModel.Path);
            // _f0Predictor = new ACFMethod(512, 16000);

            _rvcModel = new OnnxRVC(
                                    rvcModel,
                                    options.HopSize,
                                    hubertModel.Path);
        }
        finally
        {
            _initLock.Release();
        }
    }

    private void DisposeResources()
    {
        _rvcModel?.Dispose();
        _rvcModel = null;

        _f0Predictor?.Dispose();
        _f0Predictor = null;
    }
}

protected virtual void Dispose(bool disposing)
    {
        if ( _disposed )
        {
            return;
        }
        if ( disposing )
        {
            _optionsChangeRegistration?.Dispose();
            DisposeResources();
        }
        _disposed = true;
    }

using System.Buffers;
using System.Diagnostics;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Implementation of audio synthesis using ONNX models
/// </summary>
public class OnnxAudioSynthesizer : IAudioSynthesizer
{
    private readonly ITtsCache _cache;

    private readonly SemaphoreSlim _inferenceThrottle;

    private readonly IModelProvider _ittsModelProvider;

    private readonly ILogger<OnnxAudioSynthesizer> _logger;

    private readonly IOptionsMonitor<KokoroVoiceOptions> _options;

    private readonly IKokoroVoiceProvider _voiceProvider;

    private bool _disposed;

    private IReadOnlyDictionary<char, long>? _phonemeToIdMap;

    private InferenceSession? _synthesisSession;

    public OnnxAudioSynthesizer(
        IModelProvider                      ittsModelProvider,
        IKokoroVoiceProvider                voiceProvider,
        ITtsCache                           cache,
        IOptionsMonitor<KokoroVoiceOptions> options,
        ILogger<OnnxAudioSynthesizer>       logger)
    {
        _ittsModelProvider = ittsModelProvider ?? throw new ArgumentNullException(nameof(ittsModelProvider));
        _voiceProvider     = voiceProvider ?? throw new ArgumentNullException(nameof(voiceProvider));
        _cache             = cache ?? throw new ArgumentNullException(nameof(cache));
        _options           = options ?? throw new ArgumentNullException(nameof(options));
        _logger            = logger ?? throw new ArgumentNullException(nameof(logger));

        // Create throttle to limit concurrent inference operations
        _inferenceThrottle = new SemaphoreSlim(
                                               Math.Max(1, Environment.ProcessorCount / 2), // Use half of available cores for inference
                                               Environment.ProcessorCount);

        _logger.LogInformation("Initialized ONNX audio synthesizer");
    }

    /// <summary>
    ///     Synthesizes audio from phonemes
    /// </summary>
    public async Task<AudioData> SynthesizeAsync(
        string              phonemes,
        KokoroVoiceOptions? options           = null,
        CancellationToken   cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(phonemes) )
        {
            return new AudioData(Array.Empty<float>(), Array.Empty<long>());
        }

        cancellationToken.ThrowIfCancellationRequested();

        try
        {
            // Initialize lazily on first use
            await EnsureInitializedAsync(cancellationToken);

            // Get current options (to ensure we have the latest values)
            var currentOptions = options ?? _options.CurrentValue;

            // Validate phoneme length
            if ( phonemes.Length > currentOptions.MaxPhonemeLength )
            {
                _logger.LogWarning("Truncating phonemes to maximum length {MaxLength}", currentOptions.MaxPhonemeLength);
                phonemes = phonemes.Substring(0, currentOptions.MaxPhonemeLength);
            }

            // Convert phonemes to tokens
            var tokens = ConvertPhonemesToTokens(phonemes);
            if ( tokens.Count == 0 )
            {
                _logger.LogWarning("No valid tokens generated from phonemes");

                return new AudioData(Array.Empty<float>(), Array.Empty<long>());
            }

            // Get voice data
            var voice = await _voiceProvider.GetVoiceAsync(currentOptions.DefaultVoice, cancellationToken);

            // Create audio with throttling for inference
            await _inferenceThrottle.WaitAsync(cancellationToken);

            try
            {
                // Measure performance
                var timer = Stopwatch.StartNew();

                // Perform inference
                var (audioData, phonemeTimings) = await RunInferenceAsync(tokens, voice, options, cancellationToken);

                // Log performance metrics
                timer.Stop();
                LogPerformanceMetrics(timer.Elapsed, audioData.Length, phonemes.Length, options);

                return new AudioData(audioData, phonemeTimings);
            }
            finally
            {
                _inferenceThrottle.Release();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Audio synthesis was canceled");

            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during audio synthesis");

            throw;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if ( _disposed )
        {
            return;
        }

        _synthesisSession?.Dispose();
        _synthesisSession = null;

        _phonemeToIdMap = null;
        _inferenceThrottle.Dispose();

        _disposed = true;

        await Task.CompletedTask;
    }

    /// <summary>
    ///     Ensures the model and resources are initialized
    /// </summary>
    private async Task EnsureInitializedAsync(CancellationToken cancellationToken)
    {
        if ( _synthesisSession != null && _phonemeToIdMap != null )
        {
            return;
        }

        // Load model and resources
        await InitializeSessionAsync(cancellationToken);
        await LoadPhonemeMapAsync(cancellationToken);
    }

    /// <summary>
    ///     Initializes the ONNX inference session
    /// </summary>
    private async Task InitializeSessionAsync(CancellationToken cancellationToken)
    {
        if ( _synthesisSession != null )
        {
            return;
        }

        _logger.LogInformation("Initializing synthesis model");

        // Create optimized session options
        var sessionOptions = new SessionOptions {
                                                    EnableMemoryPattern    = true,
                                                    ExecutionMode          = ExecutionMode.ORT_PARALLEL,
                                                    InterOpNumThreads      = Math.Max(1, Environment.ProcessorCount / 2),
                                                    IntraOpNumThreads      = Math.Max(1, Environment.ProcessorCount / 2),
                                                    GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
                                                    LogSeverityLevel       = OrtLoggingLevel.ORT_LOGGING_LEVEL_FATAL
                                                };

        try
        {
            sessionOptions.AppendExecutionProvider_CUDA();
            _logger.LogInformation("CUDA execution provider added successfully");
        }
        catch (Exception ex)
        {
            _logger.LogWarning("CUDA execution provider not available: {Message}. Using CPU.", ex.Message);
        }

        // Get model with retry mechanism
        var        maxRetries    = 3;
        Exception? lastException = null;

        for ( var attempt = 0; attempt < maxRetries; attempt++ )
        {
            try
            {
                var model     = await _ittsModelProvider.GetModelAsync(ModelType.KokoroSynthesis, cancellationToken);
                var modelData = await model.GetDataAsync();

                _synthesisSession = new InferenceSession(modelData, sessionOptions);
                _logger.LogInformation("Synthesis model initialized successfully");

                return;
            }
            catch (Exception ex)
            {
                lastException = ex;
                _logger.LogWarning(ex, "Error initializing ONNX session (attempt {Attempt} of {MaxRetries}). Retrying...",
                                   attempt + 1, maxRetries);

                if ( attempt < maxRetries - 1 )
                {
                    // Exponential backoff
                    await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken);
                }
            }
        }

        // If we get here, all attempts failed
        throw new InvalidOperationException(
                                            $"Failed to initialize ONNX session after {maxRetries} attempts", lastException);
    }

    /// <summary>
    ///     Loads the phoneme-to-ID mapping
    /// </summary>
    private async Task LoadPhonemeMapAsync(CancellationToken cancellationToken)
    {
        if ( _phonemeToIdMap != null )
        {
            return;
        }

        _logger.LogInformation("Loading phoneme mapping");

        try
        {
            // Use cache for phoneme map to avoid repeated loading
            _phonemeToIdMap = await _cache.GetOrAddAsync("phoneme_map", async ct =>
                                                                        {
                                                                            var model   = await _ittsModelProvider.GetModelAsync(ModelType.KokoroPhonemeMappings, ct);
                                                                            var mapPath = model.Path;

                                                                            _logger.LogDebug("Loading phoneme mapping from {Path}", mapPath);

                                                                            var lines = await File.ReadAllLinesAsync(mapPath, ct);

                                                                            // Initialize with capacity for better performance
                                                                            var mapping = new Dictionary<char, long>(lines.Length);

                                                                            foreach ( var line in lines )
                                                                            {
                                                                                if ( string.IsNullOrWhiteSpace(line) || line.Length < 3 )
                                                                                {
                                                                                continue;
                                                                                }

                                                                                mapping[line[0]] = long.Parse(line[2..]);
                                                                            }

                                                                            _logger.LogInformation("Loaded {Count} phoneme mappings", mapping.Count);

                                                                            return mapping;
                                                                        }, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            /* Ignored */
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error loading phoneme mapping");

            throw;
        }
    }

    /// <summary>
    ///     Converts phoneme characters to token IDs
    /// </summary>
    private List<long> ConvertPhonemesToTokens(string phonemes)
    {
        // If no mapping loaded, can't convert
        if ( _phonemeToIdMap == null )
        {
            throw new InvalidOperationException("Phoneme map not initialized");
        }

        // Pre-allocate with expected capacity
        var tokens = new List<long>(phonemes.Length);

        foreach ( var phoneme in phonemes )
        {
            if ( _phonemeToIdMap.TryGetValue(phoneme, out var id) )
            {
                tokens.Add(id);
            }
        }

        return tokens;
    }

    /// <summary>
    ///     Runs model inference to generate audio
    /// </summary>
    private async Task<(Memory<float> AudioData, Memory<long> PhonemeTimings)> RunInferenceAsync(
        List<long>          tokens,
        VoiceData           voice,
        KokoroVoiceOptions? options           = null,
        CancellationToken   cancellationToken = default)
    {
        // Make sure session is initialized
        if ( _synthesisSession == null )
        {
            throw new InvalidOperationException("Synthesis session not initialized");
        }

        // Get current options (to ensure we have the latest values)
        var currentOptions = options ?? _options.CurrentValue;

        // Create model inputs
        var modelInputs = CreateModelInputs(tokens, voice, currentOptions.DefaultSpeed);

        // Run inference
        using var results = _synthesisSession.Run(modelInputs);

        // Extract results
        var waveformTensor = results[0].AsTensor<float>();
        var durationTensor = results[1].AsTensor<long>();

        // Extract result data
        var waveformLength = waveformTensor.Dimensions.Length > 0 ? waveformTensor.Dimensions[0] : 0;

        var waveform  = new float[waveformLength];
        var durations = new long[durationTensor.Length];

        // Copy data to results
        Buffer.BlockCopy(waveformTensor.ToArray(), 0, waveform, 0, waveformLength * sizeof(float));
        Buffer.BlockCopy(durationTensor.ToArray(), 0, durations, 0, durations.Length * sizeof(long));

        // Apply audio processing if needed
        if ( currentOptions.TrimSilence )
        {
            waveform = TrimSilence(waveform);
        }

        return (waveform, durations);
    }

    /// <summary>
    ///     Creates input tensors for the synthesis model
    /// </summary>
    private List<NamedOnnxValue> CreateModelInputs(List<long> tokens, VoiceData voice, float speed)
    {
        // Add boundary tokens (BOS/EOS)
        var tokenArray = ArrayPool<long>.Shared.Rent(tokens.Count + 2);

        try
        {
            // BOS token
            tokenArray[0] = 0;

            // Copy tokens
            tokens.CopyTo(tokenArray, 1);

            // EOS token
            tokenArray[tokens.Count + 1] = 0;

            // Create tensors
            var inputTokens = new DenseTensor<long>(
                                                    tokenArray.AsMemory(0, tokens.Count + 2),
                                                    new[] { 1, tokens.Count + 2 });

            var styleInput     = voice.GetEmbedding(inputTokens.Dimensions).ToArray();
            var voiceEmbedding = new DenseTensor<float>(styleInput, new[] { 1, styleInput.Length });

            var speedTensor = new DenseTensor<float>(
                                                     new[] { speed },
                                                     new[] { 1 });

            // Return named tensors
            return new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_ids", inputTokens), NamedOnnxValue.CreateFromTensor("style", voiceEmbedding), NamedOnnxValue.CreateFromTensor("speed", speedTensor) };
        }
        finally
        {
            // Always return the rented array
            ArrayPool<long>.Shared.Return(tokenArray);
        }
    }

    /// <summary>
    ///     Trims silence from the beginning and end of audio data
    /// </summary>
    private float[] TrimSilence(float[] audioData, float threshold = 0.01f, int minSamples = 512)
    {
        if ( audioData.Length <= minSamples * 2 )
        {
            return audioData;
        }

        // Find start (first sample above threshold)
        var startIndex = 0;
        for ( var i = 0; i < audioData.Length - minSamples; i++ )
        {
            if ( Math.Abs(audioData[i]) > threshold )
            {
                startIndex = Math.Max(0, i - minSamples);

                break;
            }
        }

        // Find end (last sample above threshold)
        var endIndex = audioData.Length - 1;
        for ( var i = audioData.Length - 1; i >= minSamples; i-- )
        {
            if ( Math.Abs(audioData[i]) > threshold )
            {
                endIndex = Math.Min(audioData.Length - 1, i + minSamples);

                break;
            }
        }

        // If no significant audio found, return original
        if ( startIndex >= endIndex )
        {
            return audioData;
        }

        // Create trimmed array
        var newLength = endIndex - startIndex + 1;
        var result    = new float[newLength];
        Array.Copy(audioData, startIndex, result, 0, newLength);

        return result;
    }

    /// <summary>
    ///     Logs performance metrics for inference
    /// </summary>
    private void LogPerformanceMetrics(TimeSpan elapsed, int audioLength, int phonemeCount, KokoroVoiceOptions? options = null)
    {
        // Get current options to ensure we have the latest sample rate
        var currentOptions = options ?? _options.CurrentValue;

        var audioDuration  = audioLength / (float)currentOptions.SampleRate;
        var elapsedSeconds = elapsed.TotalSeconds;
        var speedup        = elapsedSeconds > 0 ? audioDuration / elapsedSeconds : 0;

        _logger.LogInformation(
                               "Generated {AudioDuration:F2}s audio for {PhonemeCount} phonemes in {Elapsed:F2}s (x{Speedup:F2} real-time)",
                               audioDuration, phonemeCount, elapsedSeconds, speedup);
    }
}

using System.Diagnostics;
using System.Numerics;
using System.Threading.Channels;

using Hexa.NET.ImGui;
using Hexa.NET.ImGui.Widgets.Dialogs;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.TTS.RVC;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Editor for TTS section configuration
/// </summary>
public class TtsConfigEditor : ConfigSectionEditorBase
{
    private readonly IAudioOutputAdapter _audioPlayer;

    private readonly IRVCVoiceProvider _rvcProvider;

    private readonly IUiThemeManager _themeManager;

    private readonly ITtsEngine _ttsEngine;

    private readonly IKokoroVoiceProvider _voiceProvider;

    private List<string> _availableRVCs = new();

    private List<string> _availableVoices = new();

    private TtsConfiguration _currentConfig;

    private RVCFilterOptions _currentRvcFilterOptions;

    private KokoroVoiceOptions _currentVoiceOptions;

    private string _defaultRVC;

    private string _defaultVoice;

    private string _espeakPath;

    private bool _isPlaying = false;

    private bool _loadingRvcs = false;

    private bool _loadingVoices = false;

    private int _maxPhonemeLength;

    private string _modelDir;

    private ActiveOperation? _playbackOperation = null;

    private bool _rvcEnabled;

    private int _rvcF0UpKey;

    private int _rvcHopSize;

    private int _sampleRate;

    private float _speechRate;

    private string _testText = "This is a test of the text-to-speech system.";

    private bool _trimSilence;

    private bool _useBritishEnglish;

    public TtsConfigEditor(
        IUiConfigurationManager configManager,
        IEditorStateManager     stateManager,
        ITtsEngine              ttsEngine,
        IOutputAdapter          audioPlayer,
        IKokoroVoiceProvider    voiceProvider,
        IUiThemeManager         themeManager, IRVCVoiceProvider rvcProvider)
        : base(configManager, stateManager)
    {
        _ttsEngine     = ttsEngine;
        _audioPlayer   = (IAudioOutputAdapter)audioPlayer;
        _voiceProvider = voiceProvider;
        _themeManager  = themeManager;
        _rvcProvider   = rvcProvider;

        LoadConfiguration();

        // _audioPlayerHost.OnPlaybackStarted   += OnPlaybackStarted;
        // _audioPlayerHost.OnPlaybackCompleted += OnPlaybackCompleted;
    }

    public override string SectionKey => "TTS";

    public override string DisplayName => "TTS Configuration";

    public override void Initialize()
    {
        // Load available voices
        LoadAvailableVoicesAsync();
        LoadAvailableRVCAsync();
    }

    public override void Render()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        // Main layout with tabs for different sections
        if ( ImGui.BeginTabBar("TtsConfigTabs") )
        {
            // Basic settings tab
            if ( ImGui.BeginTabItem("Basic Settings") )
            {
                RenderTestingSection();
                RenderBasicSettings();

                ImGui.EndTabItem();
            }

            // Advanced settings tab
            if ( ImGui.BeginTabItem("Advanced Settings") )
            {
                RenderAdvancedSettings();
                ImGui.EndTabItem();
            }

            ImGui.EndTabBar();
        }

        ImGui.Spacing();
        ImGui.Separator();
        ImGui.Spacing();

        // Reset button at bottom
        ImGui.SetCursorPosX(availWidth * .5f * .5f);
        if ( ImGui.Button("Reset", new Vector2(150, 0)) )
        {
            ResetToDefaults();
        }

        ImGui.SameLine(0, 10);

        if ( ImGui.IsItemHovered() )
        {
            ImGui.SetTooltip("Reset all TTS settings to default values");
        }

        if ( !StateManager.HasUnsavedChanges )
        {
            ImGui.BeginDisabled();
            ImGui.Button("Save", new Vector2(150, 0));
            ImGui.EndDisabled();
        }
        else if ( ImGui.Button("Save", new Vector2(150, 0)) )
        {
            SaveConfiguration();
        }
    }

    public override void Update(float deltaTime)
    {
        // Simplified update just for playback operation
        if ( _playbackOperation != null && _isPlaying )
        {
            _playbackOperation.Progress += deltaTime * 0.1f;
            if ( _playbackOperation.Progress > 0.99f )
            {
                _playbackOperation.Progress = 0.99f;
            }
        }
    }

    public override void OnConfigurationChanged(ConfigurationChangedEventArgs args)
    {
        base.OnConfigurationChanged(args);

        if ( args.Type == ConfigurationChangedEventArgs.ChangeType.Reloaded )
        {
            LoadConfiguration();
        }
    }

    public override void Dispose()
    {
        // Unsubscribe from audio player events
        // _audioPlayerHost.OnPlaybackStarted   -= OnPlaybackStarted;
        // _audioPlayerHost.OnPlaybackCompleted -= OnPlaybackCompleted;

        // Cancel any active playback
        StopPlayback();

        base.Dispose();
    }

    #region Configuration Management

    private void LoadConfiguration()
    {
        _currentConfig           = ConfigManager.GetConfiguration<TtsConfiguration>("TTS");
        _currentVoiceOptions     = _currentConfig.Voice;
        _currentRvcFilterOptions = _currentConfig.Rvc;

        // Update local fields from configuration
        _modelDir          = _currentConfig.ModelDirectory;
        _espeakPath        = _currentConfig.EspeakPath;
        _speechRate        = _currentVoiceOptions.DefaultSpeed;
        _sampleRate        = _currentVoiceOptions.SampleRate;
        _trimSilence       = _currentVoiceOptions.TrimSilence;
        _useBritishEnglish = _currentVoiceOptions.UseBritishEnglish;
        _defaultVoice      = _currentVoiceOptions.DefaultVoice;
        _maxPhonemeLength  = _currentVoiceOptions.MaxPhonemeLength;

        // RVC
        _defaultRVC = _currentRvcFilterOptions.DefaultVoice;
        _rvcEnabled = _currentRvcFilterOptions.Enabled;
        _rvcHopSize = _currentRvcFilterOptions.HopSize;
        _rvcF0UpKey = _currentRvcFilterOptions.F0UpKey;
    }

    private async void LoadAvailableVoicesAsync()
    {
        try
        {
            _loadingVoices = true;

            // Register an active operation
            var operation = new ActiveOperation("load-voices", "Loading Voices");
            StateManager.RegisterActiveOperation(operation);

            // Load voices asynchronously
            var voices = await _voiceProvider.GetAvailableVoicesAsync();
            _availableVoices = voices.ToList();

            // Clear operation
            StateManager.ClearActiveOperation(operation.Id);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error loading voices: {ex.Message}");
            _availableVoices = [];
        }
        finally
        {
            _loadingVoices = false;
        }
    }

    private async void LoadAvailableRVCAsync()
    {
        try
        {
            _loadingRvcs = true;

            // Register an active operation
            var operation = new ActiveOperation("load-rvc", "Loading RVC Voices");
            StateManager.RegisterActiveOperation(operation);

            // Load voices asynchronously
            var voices = await _rvcProvider.GetAvailableVoicesAsync();
            _availableRVCs = voices.ToList();

            // Clear operation
            StateManager.ClearActiveOperation(operation.Id);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error loading voices: {ex.Message}");
            _availableRVCs = [];
        }
        finally
        {
            _loadingRvcs = false;
        }
    }

    private void UpdateConfiguration()
    {
        var updatedVoiceOptions = new KokoroVoiceOptions {
                                                             DefaultVoice      = _defaultVoice,
                                                             DefaultSpeed      = _speechRate,
                                                             SampleRate        = _sampleRate,
                                                             TrimSilence       = _trimSilence,
                                                             UseBritishEnglish = _useBritishEnglish,
                                                             MaxPhonemeLength  = _maxPhonemeLength
                                                         };

        var updatedRVCOptions = new RVCFilterOptions { DefaultVoice = _defaultRVC, Enabled = _rvcEnabled, HopSize = _rvcHopSize, F0UpKey = _rvcF0UpKey };

        var updatedConfig = new TtsConfiguration { ModelDirectory = _modelDir, EspeakPath = _espeakPath, Voice = updatedVoiceOptions, Rvc = updatedRVCOptions };

        _currentRvcFilterOptions = updatedRVCOptions;
        _currentVoiceOptions     = updatedVoiceOptions;
        _currentConfig           = updatedConfig;
        ConfigManager.UpdateConfiguration(updatedConfig, SectionKey);

        MarkAsChanged();
    }

    private void SaveConfiguration()
    {
        ConfigManager.SaveConfiguration();
        MarkAsSaved();
    }

    private void ResetToDefaults()
    {
        // Create default configuration
        var defaultVoiceOptions = new KokoroVoiceOptions();
        var defaultConfig       = new TtsConfiguration();

        // Update local state
        _currentVoiceOptions = defaultVoiceOptions;
        _currentConfig       = defaultConfig;

        // Update UI fields
        _modelDir          = defaultConfig.ModelDirectory;
        _espeakPath        = defaultConfig.EspeakPath;
        _speechRate        = defaultVoiceOptions.DefaultSpeed;
        _sampleRate        = defaultVoiceOptions.SampleRate;
        _trimSilence       = defaultVoiceOptions.TrimSilence;
        _useBritishEnglish = defaultVoiceOptions.UseBritishEnglish;
        _defaultVoice      = defaultVoiceOptions.DefaultVoice;
        _maxPhonemeLength  = defaultVoiceOptions.MaxPhonemeLength;

        // Update configuration
        ConfigManager.UpdateConfiguration(defaultConfig, "TTS");
        MarkAsChanged();
    }

    #endregion

    #region Playback Controls

    private async void StartPlayback()
    {
        if ( _isPlaying || string.IsNullOrWhiteSpace(_testText) )
        {
            return;
        }

        try
        {
            // Create a new playback operation
            _playbackOperation = new ActiveOperation("tts-playback", "Playing TTS");
            StateManager.RegisterActiveOperation(_playbackOperation);

            _isPlaying = true;

            var options = _currentVoiceOptions;

            var llmInput    = Channel.CreateUnbounded<LlmChunkEvent>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
            var ttsOutput   = Channel.CreateUnbounded<IOutputEvent>(new UnboundedChannelOptions { SingleReader  = true, SingleWriter = true });
            var audioInput  = Channel.CreateUnbounded<TtsChunkEvent>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
            var audioEvents = Channel.CreateUnbounded<IOutputEvent>(new UnboundedChannelOptions { SingleReader  = true, SingleWriter = true }); // Tts Started/Tts Ended - Audio Started/Audio Ended

            await llmInput.Writer.WriteAsync(new LlmChunkEvent(Guid.Empty, Guid.Empty, DateTimeOffset.UtcNow, _testText), _playbackOperation.CancellationSource.Token);
            llmInput.Writer.Complete();

            _ = Task.Run(async () =>
                         {
                             await foreach(var ttsOut in ttsOutput.Reader.ReadAllAsync(_playbackOperation.CancellationSource.Token))
                             {
                                 if ( ttsOut is TtsChunkEvent ttsChunk )
                                 {
                                     await audioInput.Writer.WriteAsync(ttsChunk, _playbackOperation.CancellationSource.Token);
                                 }
                             }
                         });

            _ = _ttsEngine.SynthesizeStreamingAsync(
                                                    llmInput,
                                                    ttsOutput,
                                                    Guid.Empty,
                                                    Guid.Empty,
                                                    options,
                                                    _playbackOperation.CancellationSource.Token
                                                   );

            await _audioPlayer.SendAsync(audioInput, audioEvents, Guid.Empty, _playbackOperation.CancellationSource.Token);
        }
        catch (OperationCanceledException)
        {
            Debug.WriteLine("TTS playback cancelled");
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error during TTS playback: {ex.Message}");
            _isPlaying = false;

            if ( _playbackOperation != null )
            {
                StateManager.ClearActiveOperation(_playbackOperation.Id);
                _playbackOperation = null;
            }
        }
    }

    private void StopPlayback()
    {
        if ( _playbackOperation != null )
        {
            _playbackOperation.CancellationSource.Cancel();
            StateManager.ClearActiveOperation(_playbackOperation.Id);
            _playbackOperation = null;
        }

        _isPlaying = false;
    }

    private void OnPlaybackStarted(object sender, EventArgs args)
    {
        _isPlaying = true;

        if ( _playbackOperation != null )
        {
            _playbackOperation.Progress = 0.0f;
        }
    }

    private void OnPlaybackCompleted(object sender, EventArgs args)
    {
        _isPlaying = false;

        if ( _playbackOperation != null )
        {
            _playbackOperation.Progress = 1.0f;
            StateManager.ClearActiveOperation(_playbackOperation.Id);
            _playbackOperation = null;
        }
    }

    #endregion

    #region UI Rendering Methods

    private void RenderTestingSection()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        ImGui.Spacing();
        ImGui.SeparatorText("Playground");
        ImGui.Spacing();

        // Text input frame with light background
        ImGui.Text("Test Text:");
        ImGui.SetNextItemWidth(availWidth);
        ImGui.InputTextMultiline("##TestText", ref _testText, 1000, new Vector2(0, 80));

        ImGui.Spacing();
        ImGui.Spacing();

        // Controls section with better layout
        ImGui.BeginGroup();
        {
            // Left side: Example selector
            var controlWidth = Math.Min(180, availWidth * 0.4f);
            ImGui.SetNextItemWidth(controlWidth);

            if ( ImGui.BeginCombo("##exampleLbl", "Select Example") )
            {
                string[] examples = { "Hello, world!", "The quick brown fox jumps over the lazy dog.", "Welcome to the text-to-speech system.", "How are you doing today?", "Today's date is March 3rd, 2025." };

                foreach ( var example in examples )
                {
                    var isSelected = _testText == example;
                    if ( ImGui.Selectable(example, ref isSelected) )
                    {
                        _testText = example;
                    }
                }

                ImGui.EndCombo();
            }

            ImGui.SameLine(0, 15);

            var clearDisabled = string.IsNullOrEmpty(_testText);
            if ( clearDisabled )
            {
                ImGui.BeginDisabled();
            }

            if ( ImGui.Button("Clear", new Vector2(80, 0)) && !string.IsNullOrEmpty(_testText) )
            {
                _testText = "";
            }

            if ( clearDisabled )
            {
                ImGui.EndDisabled();
            }
        }

        ImGui.EndGroup();

        ImGui.SameLine(0, 10);

        // Playback controls in a styled frame
        {
            // Play/Stop button with color styling
            if ( _isPlaying )
            {
                if ( UiStyler.AnimatedButton("Stop", new Vector2(80, 0), _isPlaying) )
                {
                    StopPlayback();
                }

                ImGui.SameLine(0, 15);
                ImGui.ProgressBar(_playbackOperation?.Progress ?? 0, new Vector2(-1, 0), "Playing");
            }
            else
            {
                var disabled = string.IsNullOrWhiteSpace(_testText);
                if ( disabled )
                {
                    ImGui.BeginDisabled();
                }

                if ( UiStyler.AnimatedButton("Play", new Vector2(80, 0), _isPlaying) )
                {
                    StartPlayback();
                }

                if ( disabled )
                {
                    ImGui.EndDisabled();
                    if ( ImGui.IsItemHovered() )
                    {
                        ImGui.SetTooltip("Enter some text to play");
                    }
                }
            }
        }
    }

    private void RenderBasicSettings()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        // === Voice Selection Section ===
        ImGui.Spacing();
        ImGui.SeparatorText("Voice Settings");
        ImGui.Spacing();

        ImGui.BeginGroup();
        {
            ImGui.SetNextItemWidth(availWidth - 120);

            if ( _loadingVoices )
            {
                ImGui.BeginDisabled();
                var loadingText = "Loading voices...";
                ImGui.InputText("##VoiceLoading", ref loadingText, 100, ImGuiInputTextFlags.ReadOnly);
                ImGui.EndDisabled();
            }
            else
            {
                if ( ImGui.BeginCombo("##VoiceSelector", string.IsNullOrEmpty(_defaultVoice) ? "<Select voice>" : _defaultVoice) )
                {
                    if ( _availableVoices.Count == 0 )
                    {
                        ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 1.0f), "No voices available");
                    }
                    else
                    {
                        foreach ( var voice in _availableVoices )
                        {
                            var isSelected = voice == _defaultVoice;
                            if ( ImGui.Selectable(voice, isSelected) )
                            {
                                _defaultVoice = voice;
                                UpdateConfiguration();
                            }

                            if ( isSelected )
                            {
                                ImGui.SetItemDefaultFocus();
                            }
                        }
                    }

                    ImGui.EndCombo();
                }
            }

            ImGui.SameLine(0, 10);

            if ( ImGui.Button("Refresh##VoiceRefresh", new Vector2(-1, 0)) )
            {
                LoadAvailableVoicesAsync();
            }

            if ( ImGui.IsItemHovered() )
            {
                ImGui.SetTooltip("Refresh available voices list");
            }

            // Speech rate slider with better styling
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Speech Rate:");
            ImGui.SameLine(120);

            ImGui.SetNextItemWidth(availWidth - 120 - 120);
            var rateChanged = ImGui.SliderFloat("##SpeechRate", ref _speechRate, 0.5f, 2.0f, "%.2f");

            ImGui.SameLine(0, 10);

            if ( ImGui.Button("Reset##Rate", new Vector2(-1, 0)) )
            {
                _speechRate = 1.0f;
                rateChanged = true;
            }

            // Voice options with checkboxes in a consistent layout
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Options:");
            ImGui.SameLine(120);

            // Trim silence
            var trimChanged = ImGui.Checkbox("Trim Silence", ref _trimSilence);
            if ( ImGui.IsItemHovered() )
            {
                ImGui.SetTooltip("Remove silence from beginning and end of speech");
            }

            ImGui.SameLine(0, 50);

            // British English
            var britishChanged = ImGui.Checkbox("Use British English", ref _useBritishEnglish);
            if ( ImGui.IsItemHovered() )
            {
                ImGui.SetTooltip("Use British English pronunciation instead of American English");
            }

            // Apply changes if needed
            if ( rateChanged || trimChanged || britishChanged )
            {
                UpdateConfiguration();
            }
        }

        ImGui.EndGroup();

        // === RVC Selection Section ===
        ImGui.Spacing();
        ImGui.SeparatorText("RVC Settings");
        ImGui.Spacing();

        ImGui.BeginGroup();
        {
            var rvcConfigChanged = ImGui.Checkbox("Enable Voice Change", ref _rvcEnabled);

            ImGui.SetNextItemWidth(availWidth - 120);

            if ( !_rvcEnabled )
            {
                ImGui.BeginDisabled();
            }

            if ( _loadingVoices )
            {
                ImGui.BeginDisabled();
                var loadingText = "Loading voices...";
                ImGui.InputText("##RVCLoading", ref loadingText, 100, ImGuiInputTextFlags.ReadOnly);
                ImGui.EndDisabled();
            }
            else
            {
                if ( ImGui.BeginCombo("##RVCSelector", string.IsNullOrEmpty(_defaultRVC) ? "<Select voice>" : _defaultRVC) )
                {
                    if ( _availableRVCs.Count == 0 )
                    {
                        ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 1.0f), "No voices available");
                    }
                    else
                    {
                        foreach ( var voice in _availableRVCs )
                        {
                            var isSelected = voice == _defaultRVC;
                            if ( ImGui.Selectable(voice, isSelected) )
                            {
                                _defaultRVC = voice;
                                UpdateConfiguration();
                            }

                            if ( isSelected )
                            {
                                ImGui.SetItemDefaultFocus();
                            }
                        }
                    }

                    ImGui.EndCombo();
                }
            }

            ImGui.SameLine(0, 10);

            if ( ImGui.Button("Refresh##RefreshRVC", new Vector2(-1, 0)) )
            {
                LoadAvailableRVCAsync();
            }

            if ( ImGui.IsItemHovered() )
            {
                ImGui.SetTooltip("Refresh available voices list");
            }

            if ( ImGui.BeginTable("RVCProps", 4, ImGuiTableFlags.SizingFixedFit) )
            {
                ImGui.TableSetupColumn("1", 100f);
                ImGui.TableSetupColumn("2", 200f);
                ImGui.TableSetupColumn("3", 100f);
                ImGui.TableSetupColumn("4", 200f);

                // Hop Size
                ImGui.TableNextRow();
                ImGui.TableNextColumn();
                ImGui.AlignTextToFramePadding();
                ImGui.Text("Hop Size");
                ImGui.TableNextColumn();
                var fontSizeChanged = ImGui.InputInt("##HopSize", ref _rvcHopSize, 8);
                if ( fontSizeChanged )
                {
                    _rvcHopSize      = Math.Clamp(_rvcHopSize, 8, 256);
                    rvcConfigChanged = true;
                }

                // F0 Key
                ImGui.TableNextColumn();
                ImGui.AlignTextToFramePadding();
                ImGui.Text("Pitch");
                ImGui.TableNextColumn();
                var pitchChanged = ImGui.InputInt("##Pitch", ref _rvcF0UpKey, 1);
                if ( pitchChanged )
                {
                    _rvcF0UpKey      = Math.Clamp(_rvcF0UpKey, -20, 20);
                    rvcConfigChanged = true;
                }

                ImGui.EndTable();
            }

            if ( !_rvcEnabled )
            {
                ImGui.EndDisabled();
            }

            if ( rvcConfigChanged )
            {
                UpdateConfiguration();
            }
        }

        ImGui.EndGroup();
    }

    private void RenderAdvancedSettings()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        // Section header
        ImGui.Spacing();
        ImGui.SeparatorText("Advanced TTS Settings");
        ImGui.Spacing();

        var sampleRateChanged = false;
        var phonemeChanged    = false;

        // ImGui.BeginDisabled();
        ImGui.BeginGroup();
        {
            ImGui.BeginDisabled();
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Sample Rate:");
            ImGui.SameLine(200);

            string[] sampleRates = ["16000 Hz (Low quality)", "24000 Hz (Standard quality)", "32000 Hz (Good quality)", "44100 Hz (High quality)", "48000 Hz (Studio quality)"];
            int[]    rateValues  = [16000, 24000, 32000, 44100, 48000];

            var currentIdx = Array.IndexOf(rateValues, _sampleRate);
            if ( currentIdx < 0 )
            {
                currentIdx = 1; // Default to 24000 Hz
            }

            ImGui.SetNextItemWidth(availWidth - 200);

            if ( ImGui.BeginCombo("##SampleRate", sampleRates[currentIdx]) )
            {
                for ( var i = 0; i < sampleRates.Length; i++ )
                {
                    var isSelected = i == currentIdx;
                    if ( ImGui.Selectable(sampleRates[i], isSelected) )
                    {
                        _sampleRate       = rateValues[i];
                        sampleRateChanged = true;
                    }

                    // Show additional info on hover
                    if ( ImGui.IsItemHovered() )
                    {
                        var tooltipText = i switch {
                            0 => "Low quality, minimal resource usage",
                            1 => "Standard quality, recommended for most uses",
                            2 => "Good quality, balanced resource usage",
                            3 => "High quality, CD audio standard",
                            4 => "Studio quality, higher resource usage",
                            _ => ""
                        };

                        ImGui.SetTooltip(tooltipText);
                    }

                    if ( isSelected )
                    {
                        ImGui.SetItemDefaultFocus();
                    }
                }

                ImGui.EndCombo();
            }

            ImGui.EndDisabled();
        }

        ImGui.EndGroup();

        ImGui.BeginGroup();
        {
            ImGui.BeginDisabled();
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Max Phoneme Length:");
            ImGui.SameLine(200);

            ImGui.SetNextItemWidth(120);
            phonemeChanged = ImGui.InputInt("##MaxPhonemeLength", ref _maxPhonemeLength);
            ImGui.EndDisabled();

            // Clamp value to valid range
            if ( phonemeChanged )
            {
                var oldValue = _maxPhonemeLength;
                _maxPhonemeLength = Math.Clamp(_maxPhonemeLength, 1, 2048);

                if ( oldValue != _maxPhonemeLength )
                {
                    ImGui.TextColored(new Vector4(1.0f, 0.7f, 0.3f, 1.0f),
                                      "Value clamped to valid range (1-2048)");
                }
            }

            // Helper text
            ImGui.Spacing();
            ImGui.TextWrapped("This is already setup correctly to work with Kokoro. Shouldn't have to change!");

            // === Paths & Resources Section ===
            ImGui.Spacing();
            ImGui.SeparatorText("Paths & Resources");
            ImGui.Spacing();

            ImGui.BeginGroup();
            {
                // Model directory
                ImGui.AlignTextToFramePadding();
                ImGui.Text("Model Directory:");
                ImGui.SameLine(150);

                ImGui.SetNextItemWidth(availWidth - 240);
                var modelDirChanged = ImGui.InputText("##ModelDir", ref _modelDir, 512, ImGuiInputTextFlags.ElideLeft);

                ImGui.SameLine(0, 10);
                if ( ImGui.Button("Browse##ModelDir", new Vector2(-1, 0)) )
                {
                    var fileDialog = new OpenFolderDialog(_modelDir);
                    fileDialog.Show();
                    if ( fileDialog.SelectedFolder != null )
                    {
                        _modelDir       = fileDialog.SelectedFolder;
                        modelDirChanged = true;
                    }
                }

                // Espeak path
                ImGui.AlignTextToFramePadding();
                ImGui.Text("Espeak Path:");
                ImGui.SameLine(150);

                ImGui.SetNextItemWidth(availWidth - 240);
                var espeakPathChanged = ImGui.InputText("##EspeakPath", ref _espeakPath, 512, ImGuiInputTextFlags.ElideLeft);

                ImGui.SameLine(0, 10);
                if ( ImGui.Button("Browse##EspeakPath", new Vector2(-1, 0)) )
                {
                    // In a real app, this would open a file browser dialog
                    Console.WriteLine("Open file browser for Espeak Path");
                }

                // Apply changes if needed
                if ( modelDirChanged || espeakPathChanged )
                {
                    UpdateConfiguration();
                }
            }

            ImGui.EndGroup();
        }

        ImGui.EndGroup();

        if ( sampleRateChanged || phonemeChanged )
        {
            UpdateConfiguration();
        }
    }

    #endregion
}

using System.Buffers;

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

namespace PersonaEngine.Lib.TTS.RVC;

public class OnnxRVC : IDisposable
{
    private readonly ArrayPool<float> _arrayPool;

    private readonly int _hopSize;

    private readonly InferenceSession _model;

    private readonly ArrayPool<short> _shortArrayPool;

    // Preallocated buffers and tensors
    private readonly DenseTensor<long> _speakerIdTensor;

    private readonly ContentVec _vecModel;

    public OnnxRVC(string modelPath, int hopsize, string vecPath)
    {
        var options = new SessionOptions {
                                             EnableMemoryPattern    = true,
                                             ExecutionMode          = ExecutionMode.ORT_PARALLEL,
                                             InterOpNumThreads      = Environment.ProcessorCount,
                                             IntraOpNumThreads      = Environment.ProcessorCount,
                                             GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
                                             LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR
                                         };

        options.AppendExecutionProvider_CUDA();

        _model    = new InferenceSession(modelPath, options);
        _hopSize  = hopsize;
        _vecModel = new ContentVec(vecPath);

        // Preallocate the speaker ID tensor
        _speakerIdTensor = new DenseTensor<long>(new[] { 1 });

        // Create array pools for temporary buffers
        _arrayPool      = ArrayPool<float>.Shared;
        _shortArrayPool = ArrayPool<short>.Shared;
    }

    public void Dispose()
    {
        _model?.Dispose();
        _vecModel?.Dispose();
    }

    public int ProcessAudio(ReadOnlyMemory<float> inputAudio,  Memory<float> outputAudio,
                            IF0Predictor          f0Predictor, int           speakerId, int f0UpKey)
    {
        // Early exit if input is empty
        if ( inputAudio.Length == 0 )
        {
            return 0;
        }

        // Check if output buffer is large enough
        if ( outputAudio.Length < inputAudio.Length )
        {
            throw new ArgumentException("Output buffer is too small", nameof(outputAudio));
        }

        // Set the speaker ID
        _speakerIdTensor[0] = speakerId;

        // Process the audio
        return ProcessInPlace(inputAudio, outputAudio, f0Predictor, _speakerIdTensor, f0UpKey);
    }

    private int ProcessInPlace(ReadOnlyMemory<float> input, Memory<float> output,
                               IF0Predictor          f0Predictor,
                               DenseTensor<long>     speakerIdTensor, int f0UpKey)
    {
        const int f0Min    = 50;
        const int f0Max    = 1100;
        var       f0MelMin = 1127 * Math.Log(1 + f0Min / 700.0);
        var       f0MelMax = 1127 * Math.Log(1 + f0Max / 700.0);

        if ( input.Length / 16000.0 > 30.0 )
        {
            throw new Exception("Audio segment is too long (>30s)");
        }

        // Calculate original scale for normalization (matching original implementation)
        var minValue  = float.MaxValue;
        var maxValue  = float.MinValue;
        var inputSpan = input.Span;
        for ( var i = 0; i < input.Length; i++ )
        {
            minValue = Math.Min(minValue, inputSpan[i]);
            maxValue = Math.Max(maxValue, inputSpan[i]);
        }

        var originalScale = maxValue - minValue;

        // Get the hubert features
        var hubert = _vecModel.Forward(input);

        // Repeat and transpose the features
        var hubertRepeated = RVCUtils.RepeatTensor(hubert, 2);
        hubertRepeated = RVCUtils.Transpose(hubertRepeated, 0, 2, 1);

        hubert = null; // Allow for garbage collection

        var hubertLength       = hubertRepeated.Dimensions[1];
        var hubertLengthTensor = new DenseTensor<long>(new[] { 1 }) { [0] = hubertLength };

        // Allocate buffers for F0 calculations
        var f0Buffer = _arrayPool.Rent(hubertLength);
        var f0Memory = new Memory<float>(f0Buffer, 0, hubertLength);

        try
        {
            // Calculate F0 directly into buffer
            f0Predictor.ComputeF0(input, f0Memory, hubertLength);

            // Create pitch tensors
            var pitchBuffer  = _arrayPool.Rent(hubertLength);
            var pitchTensor  = new DenseTensor<long>(new[] { 1, hubertLength });
            var pitchfTensor = new DenseTensor<float>(new[] { 1, hubertLength });

            try
            {
                // Apply pitch shift and convert to mel scale
                for ( var i = 0; i < hubertLength; i++ )
                {
                    // Apply pitch shift
                    var shiftedF0 = f0Buffer[i] * (float)Math.Pow(2, f0UpKey / 12.0);
                    pitchfTensor[0, i] = shiftedF0;

                    // Convert to mel scale for pitch
                    var f0Mel = 1127 * Math.Log(1 + shiftedF0 / 700.0);
                    if ( f0Mel > 0 )
                    {
                        f0Mel = (f0Mel - f0MelMin) * 254 / (f0MelMax - f0MelMin) + 1;
                        f0Mel = Math.Round(f0Mel);
                    }

                    if ( f0Mel <= 1 )
                    {
                        f0Mel = 1;
                    }

                    if ( f0Mel > 255 )
                    {
                        f0Mel = 255;
                    }

                    pitchTensor[0, i] = (long)f0Mel;
                }

                // Generate random noise tensor
                var rndTensor = new DenseTensor<float>(new[] { 1, 192, hubertLength });
                var random    = new Random();
                for ( var i = 0; i < 192 * hubertLength; i++ )
                {
                    rndTensor[0, i / hubertLength, i % hubertLength] = (float)random.NextDouble();
                }

                // Run the model
                var outWav = Forward(hubertRepeated, hubertLengthTensor, pitchTensor,
                                     pitchfTensor, speakerIdTensor, rndTensor);

                // Apply padding to match original implementation
                // (adding padding at the end only, like in original Pad method)
                var paddedSize   = outWav.Length + 2 * _hopSize;
                var paddedOutput = _shortArrayPool.Rent(paddedSize);
                try
                {
                    // Copy original output to the beginning of padded output
                    for ( var i = 0; i < outWav.Length; i++ )
                    {
                        paddedOutput[i] = outWav[i];
                    }
                    // Rest of array is already zeroed when rented from pool

                    // Find min and max values for normalization
                    var minOutValue = short.MaxValue;
                    var maxOutValue = short.MinValue;
                    for ( var i = 0; i < outWav.Length; i++ )
                    {
                        minOutValue = Math.Min(minOutValue, outWav[i]);
                        maxOutValue = Math.Max(maxOutValue, outWav[i]);
                    }

                    // Copy the output to the buffer with normalization matching original
                    var outputSpan = output.Span;
                    if ( outputSpan.Length < paddedSize )
                    {
                        throw new InvalidOperationException($"Output buffer too small. Needed {paddedSize}, but only had {outputSpan.Length}");
                    }

                    var maxLen = Math.Min(paddedSize, outputSpan.Length);

                    // Apply normalization that matches the original implementation
                    float range = maxOutValue - minOutValue;
                    if ( range > 0 )
                    {
                        for ( var i = 0; i < maxLen; i++ )
                        {
                            outputSpan[i] = paddedOutput[i] * originalScale / range;
                        }
                    }
                    else
                    {
                        // Handle edge case where all values are the same
                        for ( var i = 0; i < maxLen; i++ )
                        {
                            outputSpan[i] = 0;
                        }
                    }

                    return outWav.Length;
                }
                finally
                {
                    _shortArrayPool.Return(paddedOutput);
                }
            }
            finally
            {
                _arrayPool.Return(pitchBuffer);
            }
        }
        finally
        {
            _arrayPool.Return(f0Buffer);
        }
    }

    private short[] Forward(DenseTensor<float> hubert,    DenseTensor<long>  hubertLength,
                            DenseTensor<long>  pitch,     DenseTensor<float> pitchf,
                            DenseTensor<long>  speakerId, DenseTensor<float> noise)
    {
        var inputs = new List<NamedOnnxValue> {
                                                  NamedOnnxValue.CreateFromTensor(_model.InputMetadata.Keys.ElementAt(0), hubert),
                                                  NamedOnnxValue.CreateFromTensor(_model.InputMetadata.Keys.ElementAt(1), hubertLength),
                                                  NamedOnnxValue.CreateFromTensor(_model.InputMetadata.Keys.ElementAt(2), pitch),
                                                  NamedOnnxValue.CreateFromTensor(_model.InputMetadata.Keys.ElementAt(3), pitchf),
                                                  NamedOnnxValue.CreateFromTensor(_model.InputMetadata.Keys.ElementAt(4), speakerId),
                                                  NamedOnnxValue.CreateFromTensor(_model.InputMetadata.Keys.ElementAt(5), noise)
                                              };

        var results = _model.Run(inputs);
        var output  = results.First().AsTensor<float>();

        return output.Select(x => (short)(x * 32767)).ToArray();
    }
}

using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Channels;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.LLM;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Main TTS engine implementation
/// </summary>
public class TtsEngine : ITtsEngine
{
    private readonly IList<IAudioFilter> _audioFilters;

    private readonly ILogger<TtsEngine> _logger;

    private readonly IOptionsMonitor<KokoroVoiceOptions> _options;

    private readonly IPhonemizer _phonemizer;

    private readonly IAudioSynthesizer _synthesizer;

    private readonly IList<ITextFilter> _textFilters;

    private readonly ITextProcessor _textProcessor;

    private readonly SemaphoreSlim _throttle;

    private bool _disposed;

    public TtsEngine(
        ITextProcessor                      textProcessor,
        IPhonemizer                         phonemizer,
        IAudioSynthesizer                   synthesizer,
        IOptionsMonitor<KokoroVoiceOptions> options,
        IEnumerable<IAudioFilter>           audioFilters,
        IEnumerable<ITextFilter>            textFilters,
        ILoggerFactory                      loggerFactory)
    {
        _textProcessor = textProcessor ?? throw new ArgumentNullException(nameof(textProcessor));
        _phonemizer    = phonemizer ?? throw new ArgumentNullException(nameof(phonemizer));
        _synthesizer   = synthesizer ?? throw new ArgumentNullException(nameof(synthesizer));
        _options       = options;
        _audioFilters  = audioFilters.OrderByDescending(x => x.Priority).ToList();
        _textFilters   = textFilters.OrderByDescending(x => x.Priority).ToList();
        _logger        = loggerFactory?.CreateLogger<TtsEngine>() ?? throw new ArgumentNullException(nameof(loggerFactory));

        _throttle = new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount);
    }

    public void Dispose()
    {
        if ( _disposed )
        {
            return;
        }

        _throttle.Dispose();
        _disposed = true;
    }

    public async Task<CompletionReason> SynthesizeStreamingAsync(
        ChannelReader<LlmChunkEvent> inputReader,
        ChannelWriter<IOutputEvent>  outputWriter,
        Guid                         turnId,
        Guid                         sessionId,
        KokoroVoiceOptions?          options           = null,
        CancellationToken            cancellationToken = default
    )
    {
        var textBuffer      = new StringBuilder(4096);
        var completedReason = CompletionReason.Completed;
        var firstChunk      = true;

        try
        {
            await foreach ( var (_, _, _, textChunk) in inputReader.ReadAllAsync(cancellationToken).ConfigureAwait(false) )
            {
                if ( cancellationToken.IsCancellationRequested )
                {
                    break;
                }

                if ( string.IsNullOrEmpty(textChunk) )
                {
                    continue;
                }

                textBuffer.Append(textChunk);
                var currentText = textBuffer.ToString();

                var processedText = await _textProcessor.ProcessAsync(currentText, cancellationToken);
                var sentences     = processedText.Sentences;

                if ( sentences.Count <= 1 )
                {
                    continue;
                }

                // Process all sentences except the last one(which might be incomplete)
                for ( var i = 0; i < sentences.Count - 1; i++ )
                {
                    var sentence = sentences[i].Trim();
                    if ( string.IsNullOrWhiteSpace(sentence) )
                    {
                        continue;
                    }

                    await foreach ( var segment in ProcessSentenceAsync(sentence, options, cancellationToken) )
                    {
                        ApplyAudioFilters(segment);

                        if ( firstChunk )
                        {
                            var firstChunkEvent = new TtsStreamStartEvent(sessionId, turnId, DateTimeOffset.UtcNow);
                            await outputWriter.WriteAsync(firstChunkEvent, cancellationToken).ConfigureAwait(false);

                            firstChunk = false;
                        }

                        var chunkEvent = new TtsChunkEvent(sessionId, turnId, DateTimeOffset.UtcNow, segment);
                        await outputWriter.WriteAsync(chunkEvent, cancellationToken).ConfigureAwait(false);
                    }
                }

                textBuffer.Clear();
                textBuffer.Append(sentences[^1]);
            }

            var remainingText = textBuffer.ToString().Trim();
            if ( !string.IsNullOrEmpty(remainingText) )
            {
                await foreach ( var segment in ProcessSentenceAsync(remainingText, options, cancellationToken) )
                {
                    ApplyAudioFilters(segment);

                    if ( firstChunk )
                    {
                        var firstChunkEvent = new TtsStreamStartEvent(sessionId, turnId, DateTimeOffset.UtcNow);
                        await outputWriter.WriteAsync(firstChunkEvent, cancellationToken).ConfigureAwait(false);

                        firstChunk = false;
                    }

                    var chunkEvent = new TtsChunkEvent(sessionId, turnId, DateTimeOffset.UtcNow, segment);
                    await outputWriter.WriteAsync(chunkEvent, cancellationToken).ConfigureAwait(false);
                }
            }
        }
        catch (OperationCanceledException)
        {
            completedReason = CompletionReason.Cancelled;
        }
        catch (Exception ex)
        {
            completedReason = CompletionReason.Error;

            await outputWriter.WriteAsync(new ErrorOutputEvent(sessionId, turnId, DateTimeOffset.UtcNow, ex), cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            if ( !firstChunk )
            {
                await outputWriter.WriteAsync(new TtsStreamEndEvent(sessionId, turnId, DateTimeOffset.UtcNow, completedReason), cancellationToken).ConfigureAwait(false);
            }
        }

        return completedReason;
    }

    /// <summary>
    ///     Processes a single sentence for synthesis
    /// </summary>
    private async IAsyncEnumerable<AudioSegment> ProcessSentenceAsync(
        string                                     sentence,
        KokoroVoiceOptions?                        options           = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrWhiteSpace(sentence) )
        {
            yield break;
        }

        await _throttle.WaitAsync(cancellationToken);

        var currentOptions = options ?? _options.CurrentValue;

        try
        {
            // Apply text filters before processing
            var processedText     = sentence;
            var textFilterResults = new List<TextFilterResult>(_textFilters.Count);

            foreach ( var textFilter in _textFilters )
            {
                var filterResult = await textFilter.ProcessAsync(processedText, cancellationToken);
                processedText = filterResult.ProcessedText;

                textFilterResults.Add(filterResult);
            }

            // Get phonemes
            var phonemeResult = await _phonemizer.ToPhonemesAsync(processedText, cancellationToken);

            // Process each phoneme chunk
            foreach ( var phonemeChunk in SplitPhonemes(phonemeResult.Phonemes, 510) )
            {
                // Get audio
                var audioData = await _synthesizer.SynthesizeAsync(
                                                                   phonemeChunk,
                                                                   currentOptions,
                                                                   cancellationToken);

                // Apply timing
                ApplyTokenTimings(
                                  phonemeResult.Tokens,
                                  currentOptions,
                                  audioData.PhonemeTimings);

                // Return segment
                var segment = new AudioSegment(
                                               audioData.Samples,
                                               options?.SampleRate ?? _options.CurrentValue.SampleRate,
                                               phonemeResult.Tokens);

                for ( var index = 0; index < _textFilters.Count; index++ )
                {
                    var textFilter       = _textFilters[index];
                    var textFilterResult = textFilterResults[index];
                    await textFilter.PostProcessAsync(textFilterResult, segment, cancellationToken);
                }

                yield return segment;
            }
        }
        finally
        {
            _throttle.Release();
        }
    }

    /// <summary>
    ///     Splits phonemes into manageable chunks
    /// </summary>
    private IEnumerable<string> SplitPhonemes(string phonemes, int maxLength)
    {
        if ( string.IsNullOrEmpty(phonemes) )
        {
            yield return string.Empty;

            yield break;
        }

        if ( phonemes.Length <= maxLength )
        {
            yield return phonemes;

            yield break;
        }

        var currentIndex = 0;
        while ( currentIndex < phonemes.Length )
        {
            var remainingLength = phonemes.Length - currentIndex;
            var chunkSize       = Math.Min(maxLength, remainingLength);

            // Find a good breakpoint (whitespace or punctuation)
            if ( chunkSize < remainingLength && chunkSize > 10 )
            {
                // Look backwards from the end to find a good breakpoint
                for ( var i = currentIndex + chunkSize - 1; i > currentIndex + 10; i-- )
                {
                    if ( char.IsWhiteSpace(phonemes[i]) || IsPunctuation(phonemes[i]) )
                    {
                        chunkSize = i - currentIndex + 1;

                        break;
                    }
                }
            }

            yield return phonemes.Substring(currentIndex, chunkSize);
            currentIndex += chunkSize;
        }
    }

    private bool IsPunctuation(char c) { return ".,:;!?-—()[]{}\"'".Contains(c); }

    private void ApplyAudioFilters(AudioSegment audioSegment)
    {
        foreach ( var audioFilter in _audioFilters )
        {
            audioFilter.Process(audioSegment);
        }
    }

    /// <summary>
    ///     Applies timing information to tokens
    /// </summary>
    private void ApplyTokenTimings(
        IReadOnlyList<Token> tokens,
        KokoroVoiceOptions   options,
        ReadOnlyMemory<long> phonemeTimings)
    {
        // Skip if no timing information
        if ( tokens.Count == 0 || phonemeTimings.Length < 3 )
        {
            return;
        }

        var timingsSpan = phonemeTimings.Span;

        // Magic scaling factor for timing conversion
        const int TIME_DIVISOR = 80;

        // Start with boundary tokens (often <bos> token)
        var leftTime  = options.TrimSilence ? 0 : 2 * Math.Max(0, timingsSpan[0] - 3);
        var rightTime = leftTime;

        // Process each token
        var timingIndex = 1;
        foreach ( var token in tokens )
        {
            // Skip tokens without phonemes
            if ( string.IsNullOrEmpty(token.Phonemes) )
            {
                // Handle whitespace timing specially
                if ( token.Whitespace == " " && timingIndex + 1 < timingsSpan.Length )
                {
                    timingIndex++;
                    leftTime  = rightTime + timingsSpan[timingIndex];
                    rightTime = leftTime + timingsSpan[timingIndex];
                    timingIndex++;
                }

                continue;
            }

            // Calculate end index for this token's phonemes
            var endIndex = timingIndex + (token.Phonemes?.Length ?? 0);
            if ( endIndex >= phonemeTimings.Length )
            {
                continue;
            }

            // Start time for this token
            var startTime = (double)leftTime / TIME_DIVISOR;

            // Sum durations for all phonemes in this token
            var tokenDuration = 0L;
            for ( var i = timingIndex; i < endIndex && i < timingsSpan.Length; i++ )
            {
                tokenDuration += timingsSpan[i];
            }

            // Handle whitespace after token
            var spaceDuration = token.Whitespace == " " && endIndex < timingsSpan.Length
                                    ? timingsSpan[endIndex]
                                    : 0;

            // Calculate end time
            leftTime = rightTime + 2 * tokenDuration + spaceDuration;
            var endTime = (double)leftTime / TIME_DIVISOR;
            rightTime = leftTime + spaceDuration;

            // Add token with timing
            token.StartTs = startTime;
            token.EndTs   = endTime;

            // Move to next token's timing
            timingIndex = endIndex + (token.Whitespace == " " ? 1 : 0);
        }
    }
}

using System.Buffers.Binary;

namespace PersonaEngine.Lib.Audio;

public class PcmResampler
{
    // Constants for audio format
    private const int BYTES_PER_SAMPLE = 2;

    private const int MAX_FRAME_SIZE = 1920;

    private readonly float[] _filterCoefficients;

    private readonly int _filterDelay;

    // Filter configuration
    private readonly int _filterTaps;

    private readonly float[] _floatHistory;

    private readonly short[] _history;

    // Sample rate configuration

    // Pre-allocated working buffers
    private readonly short[] _inputSamples;

    private int _historyLength;

    // State tracking
    private float _position = 0.0f;

    public PcmResampler(int inputSampleRate = 48000, int outputSampleRate = 16000)
    {
        if ( inputSampleRate <= 0 || outputSampleRate <= 0 )
        {
            throw new ArgumentException("Sample rates must be positive values");
        }

        InputSampleRate  = inputSampleRate;
        OutputSampleRate = outputSampleRate;
        ResampleRatio    = (float)InputSampleRate / OutputSampleRate;

        _filterTaps  = DetermineOptimalFilterTaps(ResampleRatio);
        _filterDelay = _filterTaps / 2;

        var cutoffFrequency = Math.Min(0.45f * OutputSampleRate, 0.9f * OutputSampleRate / 2) / InputSampleRate;
        _filterCoefficients = GenerateLowPassFilter(_filterTaps, cutoffFrequency);

        _history       = new short[_filterTaps + 10];
        _floatHistory  = new float[_filterTaps + 10];
        _historyLength = 0;

        _inputSamples = new short[MAX_FRAME_SIZE];
    }

    public float ResampleRatio { get; }

    public int InputSampleRate { get; }

    public int OutputSampleRate { get; }

    private int DetermineOptimalFilterTaps(float ratio)
    {
        if ( Math.Abs(ratio - Math.Round(ratio)) < 0.01f )
        {
            return Math.Max(24, (int)(12 * ratio));
        }

        return Math.Max(36, (int)(18 * ratio));
    }

    private float[] GenerateLowPassFilter(int taps, float cutoff)
    {
        var coefficients = new float[taps];
        var center       = taps / 2;

        var sum = 0.0;
        for ( var i = 0; i < taps; i++ )
        {
            if ( i == center )
            {
                coefficients[i] = (float)(2.0 * Math.PI * cutoff);
            }
            else
            {
                var x = 2.0 * Math.PI * cutoff * (i - center);
                coefficients[i] = (float)(Math.Sin(x) / x);
            }

            var window = 0.42 - 0.5 * Math.Cos(2.0 * Math.PI * i / (taps - 1))
                         + 0.08 * Math.Cos(4.0 * Math.PI * i / (taps - 1));

            coefficients[i] *= (float)window;

            sum += coefficients[i];
        }

        for ( var i = 0; i < taps; i++ )
        {
            coefficients[i] /= (float)sum;
        }

        return coefficients;
    }

    public int Process(ReadOnlySpan<byte> input, Span<byte> output)
    {
        var inputSampleCount = input.Length / BYTES_PER_SAMPLE;
        ConvertToShorts(input, _inputSamples, inputSampleCount);

        var maxOutputSamples = (int)Math.Ceiling(inputSampleCount / ResampleRatio) + 2;
        if ( output.Length < maxOutputSamples * BYTES_PER_SAMPLE )
        {
            throw new ArgumentException("Output buffer is too small for the resampled data");
        }

        var outputIndex = 0;

        while ( _position < inputSampleCount )
        {
            float sum       = 0;
            var   baseIndex = (int)Math.Floor(_position);

            for ( var tap = 0; tap < _filterTaps; tap++ )
            {
                var sampleIndex = baseIndex - _filterDelay + tap;
                var sample      = GetSampleWithHistory(sampleIndex, _inputSamples, inputSampleCount);
                sum += sample * _filterCoefficients[tap];
            }

            var outputValue = (short)Math.Clamp((int)Math.Round(sum), short.MinValue, short.MaxValue);
            if ( outputIndex < maxOutputSamples )
            {
                BinaryPrimitives.WriteInt16LittleEndian(
                                                        output.Slice(outputIndex * BYTES_PER_SAMPLE, BYTES_PER_SAMPLE), outputValue);

                outputIndex++;
            }

            _position += ResampleRatio;
        }

        UpdateHistory(_inputSamples, inputSampleCount);
        _position -= inputSampleCount;

        return outputIndex * BYTES_PER_SAMPLE;
    }

    public int Process(Stream input, Memory<byte> output)
    {
        var buffer    = new byte[MAX_FRAME_SIZE * BYTES_PER_SAMPLE];
        var bytesRead = input.Read(buffer, 0, buffer.Length);

        return Process(buffer.AsSpan(0, bytesRead), output.Span);
    }

    public int ProcessInPlace(Span<byte> buffer)
    {
        if ( ResampleRatio < 1.0f )
        {
            throw new InvalidOperationException("In-place resampling only supports downsampling (input rate > output rate)");
        }

        var inputSampleCount = buffer.Length / BYTES_PER_SAMPLE;

        // Make a copy of the input for processing
        Span<short> inputCopy = stackalloc short[inputSampleCount];
        for ( var i = 0; i < inputSampleCount; i++ )
        {
            inputCopy[i] = BinaryPrimitives.ReadInt16LittleEndian(buffer.Slice(i * BYTES_PER_SAMPLE, BYTES_PER_SAMPLE));
        }

        var expectedOutputCount = (int)Math.Ceiling(inputSampleCount / ResampleRatio);

        // Calculate positions and work from last to first
        var outputIndex  = expectedOutputCount - 1;
        var lastPosition = _position + (inputSampleCount - 1) - ResampleRatio * (expectedOutputCount - 1);

        while ( lastPosition >= 0 && outputIndex >= 0 )
        {
            float sum       = 0;
            var   baseIndex = (int)Math.Floor(lastPosition);

            for ( var tap = 0; tap < _filterTaps; tap++ )
            {
                var   sampleIndex = baseIndex - _filterDelay + tap;
                short sample;

                if ( sampleIndex >= 0 && sampleIndex < inputSampleCount )
                {
                    sample = inputCopy[sampleIndex];
                }
                else if ( sampleIndex < 0 && -sampleIndex <= _historyLength )
                {
                    sample = _history[_historyLength + sampleIndex];
                }
                else
                {
                    sample = 0;
                }

                sum += sample * _filterCoefficients[tap];
            }

            var outputValue = (short)Math.Clamp((int)Math.Round(sum), short.MinValue, short.MaxValue);
            BinaryPrimitives.WriteInt16LittleEndian(
                                                    buffer.Slice(outputIndex * BYTES_PER_SAMPLE, BYTES_PER_SAMPLE), outputValue);

            outputIndex--;
            lastPosition -= ResampleRatio;
        }

        // We need to keep the input history despite having processed in-place
        UpdateHistory(inputCopy.ToArray(), inputSampleCount);
        _position = _position + inputSampleCount - ResampleRatio * expectedOutputCount;

        return expectedOutputCount * BYTES_PER_SAMPLE;
    }

    public int ProcessFloat(ReadOnlySpan<float> input, Span<float> output)
    {
        var inputSampleCount = input.Length;

        var maxOutputSamples = (int)Math.Ceiling(inputSampleCount / ResampleRatio) + 2;
        if ( output.Length < maxOutputSamples )
        {
            throw new ArgumentException("Output buffer is too small for the resampled data");
        }

        var outputIndex = 0;

        while ( _position < inputSampleCount )
        {
            float sum       = 0;
            var   baseIndex = (int)Math.Floor(_position);

            for ( var tap = 0; tap < _filterTaps; tap++ )
            {
                var sampleIndex = baseIndex - _filterDelay + tap;
                var sample      = GetFloatSampleWithHistory(sampleIndex, input);
                sum += sample * _filterCoefficients[tap];
            }

            if ( outputIndex < maxOutputSamples )
            {
                output[outputIndex] = sum;
                outputIndex++;
            }

            _position += ResampleRatio;
        }

        UpdateFloatHistory(input);
        _position -= inputSampleCount;

        return outputIndex;
    }

    public int ProcessFloatInPlace(Span<float> buffer)
    {
        // For in-place, we must work backward to avoid overwriting unprocessed input
        if ( ResampleRatio < 1.0f )
        {
            throw new InvalidOperationException("In-place resampling only supports downsampling (input rate > output rate)");
        }

        var inputSampleCount    = buffer.Length;
        var expectedOutputCount = (int)Math.Ceiling(inputSampleCount / ResampleRatio);

        // First, store the full input for history and reference
        var inputCopy = new float[inputSampleCount];
        buffer.CopyTo(inputCopy);

        // Calculate sample positions and work from last to first
        var outputIndex  = expectedOutputCount - 1;
        var lastPosition = _position + (inputSampleCount - 1) - ResampleRatio * (expectedOutputCount - 1);

        while ( lastPosition >= 0 && outputIndex >= 0 )
        {
            float sum       = 0;
            var   baseIndex = (int)Math.Floor(lastPosition);

            for ( var tap = 0; tap < _filterTaps; tap++ )
            {
                var   sampleIndex = baseIndex - _filterDelay + tap;
                float sample;

                if ( sampleIndex >= 0 && sampleIndex < inputSampleCount )
                {
                    sample = inputCopy[sampleIndex];
                }
                else if ( sampleIndex < 0 && -sampleIndex <= _historyLength )
                {
                    sample = _floatHistory[_historyLength + sampleIndex];
                }
                else
                {
                    sample = 0f;
                }

                sum += sample * _filterCoefficients[tap];
            }

            buffer[outputIndex] = sum;
            outputIndex--;
            lastPosition -= ResampleRatio;
        }

        UpdateFloatHistory(inputCopy);
        _position = _position + inputSampleCount - ResampleRatio * expectedOutputCount;

        return expectedOutputCount;
    }

    private short GetSampleWithHistory(int index, short[] inputSamples, int inputSampleCount)
    {
        if ( index >= 0 && index < inputSampleCount )
        {
            return inputSamples[index];
        }

        if ( index < 0 && -index <= _historyLength )
        {
            return _history[_historyLength + index];
        }

        return 0;
    }

    private float GetFloatSampleWithHistory(int index, ReadOnlySpan<float> inputSamples)
    {
        if ( index >= 0 && index < inputSamples.Length )
        {
            return inputSamples[index];
        }

        if ( index < 0 && -index <= _historyLength )
        {
            return _floatHistory[_historyLength + index];
        }

        return 0f;
    }

    private void UpdateHistory(short[] currentFrame, int frameLength)
    {
        var samplesToKeep = Math.Min(frameLength, _history.Length);

        if ( samplesToKeep > 0 )
        {
            var unusedHistorySamples = Math.Min(_historyLength, _history.Length - samplesToKeep);
            if ( unusedHistorySamples > 0 )
            {
                Array.Copy(_history, _historyLength - unusedHistorySamples, _history, 0, unusedHistorySamples);
            }

            Array.Copy(currentFrame, frameLength - samplesToKeep, _history, unusedHistorySamples, samplesToKeep);
            _historyLength = unusedHistorySamples + samplesToKeep;
        }

        _historyLength = Math.Min(_historyLength, _history.Length);
    }

    private void UpdateFloatHistory(ReadOnlySpan<float> currentFrame)
    {
        var samplesToKeep = Math.Min(currentFrame.Length, _floatHistory.Length);

        if ( samplesToKeep > 0 )
        {
            var unusedHistorySamples = Math.Min(_historyLength, _floatHistory.Length - samplesToKeep);
            if ( unusedHistorySamples > 0 )
            {
                Array.Copy(_floatHistory, _historyLength - unusedHistorySamples, _floatHistory, 0, unusedHistorySamples);
            }

            for ( var i = 0; i < samplesToKeep; i++ )
            {
                _floatHistory[unusedHistorySamples + i] = currentFrame[currentFrame.Length - samplesToKeep + i];
            }

            _historyLength = unusedHistorySamples + samplesToKeep;
        }

        _historyLength = Math.Min(_historyLength, _floatHistory.Length);
    }

    private void ConvertToShorts(ReadOnlySpan<byte> input, short[] output, int sampleCount)
    {
        for ( var i = 0; i < sampleCount; i++ )
        {
            output[i] = BinaryPrimitives.ReadInt16LittleEndian(input.Slice(i * BYTES_PER_SAMPLE, BYTES_PER_SAMPLE));
        }
    }

    public void Reset()
    {
        _position      = 0;
        _historyLength = 0;
        Array.Clear(_history, 0, _history.Length);
        Array.Clear(_floatHistory, 0, _floatHistory.Length);
    }
}

using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.ASR.VAD;
using PersonaEngine.Lib.Audio;

namespace PersonaEngine.Lib.ASR.Transcriber;

internal class RealtimeTranscriptor : IRealtimeSpeechTranscriptor, IAsyncDisposable
{
    private readonly object cacheLock = new();

    private readonly ILogger<RealtimeTranscriptor> logger;

    private readonly RealtimeSpeechTranscriptorOptions options;

    private readonly RealtimeOptions realtimeOptions;

    private readonly ISpeechTranscriptorFactory? recognizingSpeechTranscriptorFactory;

    private readonly ISpeechTranscriptorFactory speechTranscriptorFactory;

    private readonly Dictionary<string, ISpeechTranscriptor> transcriptorCache;

    private readonly IVadDetector vadDetector;

    private TimeSpan _lastDuration = TimeSpan.Zero;

    private TimeSpan _processedDuration = TimeSpan.Zero;

    private bool isDisposed;

    public RealtimeTranscriptor(
        ISpeechTranscriptorFactory        speechTranscriptorFactory,
        IVadDetector                      vadDetector,
        ISpeechTranscriptorFactory?       recognizingSpeechTranscriptorFactory,
        RealtimeSpeechTranscriptorOptions options,
        RealtimeOptions                   realtimeOptions,
        ILogger<RealtimeTranscriptor>     logger)
    {
        this.speechTranscriptorFactory            = speechTranscriptorFactory;
        this.vadDetector                          = vadDetector;
        this.recognizingSpeechTranscriptorFactory = recognizingSpeechTranscriptorFactory;
        this.options                              = options;
        this.realtimeOptions                      = realtimeOptions;
        this.logger                               = logger;
        transcriptorCache                         = new Dictionary<string, ISpeechTranscriptor>();

        logger.LogDebug("RealtimeTranscriptor initialized with options: {@Options}, realtime options: {@RealtimeOptions}",
                        options, realtimeOptions);
    }

    public async ValueTask DisposeAsync()
    {
        if ( isDisposed )
        {
            return;
        }

        // lock (cacheLock)
        // {
        logger.LogInformation("Disposing {Count} cached transcriptors", transcriptorCache.Count);
        foreach ( var transcriptor in transcriptorCache.Values )
        {
            await transcriptor.DisposeAsync();
        }

        transcriptorCache.Clear();
        // }

        isDisposed = true;
        GC.SuppressFinalize(this);
    }

    public async IAsyncEnumerable<IRealtimeRecognitionEvent> TranscribeAsync(
        IAwaitableAudioSource                      source,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        if ( isDisposed )
        {
            throw new ObjectDisposedException(nameof(RealtimeTranscriptor));
        }

        var          stopwatch        = Stopwatch.StartNew();
        var          promptBuilder    = new StringBuilder(options.Prompt);
        CultureInfo? detectedLanguage = null;

        await source.WaitForInitializationAsync(cancellationToken);
        var sessionId = Guid.NewGuid().ToString();

        logger.LogInformation("Starting transcription session {SessionId}", sessionId);

        yield return new RealtimeSessionStarted(sessionId);

        try
        {
            while ( !source.IsFlushed )
            {
                var currentDuration = source.Duration;

                if ( currentDuration == _lastDuration )
                {
                    await source.WaitForNewSamplesAsync(_lastDuration + realtimeOptions.ProcessingInterval, cancellationToken);

                    continue;
                }

                logger.LogTrace("Processing new audio segment: Current={Current}ms, Last={Last}ms, Delta={Delta}ms",
                                currentDuration.TotalMilliseconds,
                                _lastDuration.TotalMilliseconds,
                                (currentDuration - _lastDuration).TotalMilliseconds);

                _lastDuration = currentDuration;
                var segmentStopwatch = Stopwatch.StartNew();
                var slicedSource     = new SliceAudioSource(source, _processedDuration, currentDuration - _processedDuration);

                VadSegment? lastNonFinalSegment = null;
                VadSegment? recognizingSegment  = null;

                await foreach ( var segment in vadDetector.DetectSegmentsAsync(slicedSource, cancellationToken) )
                {
                    if ( segment.IsIncomplete )
                    {
                        recognizingSegment = segment;

                        continue;
                    }

                    var segmentEnd = segment.StartTime + segment.Duration;
                    lastNonFinalSegment = segment;

                    logger.LogDebug("Processing VAD segment: Start={Start}ms, Duration={Duration}ms",
                                    segment.StartTime.TotalMilliseconds,
                                    segment.Duration.TotalMilliseconds);

                    var transcribeStopwatch = Stopwatch.StartNew();
                    var transcribingEvents = TranscribeSegments(
                                                                speechTranscriptorFactory,
                                                                source,
                                                                _processedDuration,
                                                                segment.StartTime,
                                                                segment.Duration,
                                                                promptBuilder,
                                                                detectedLanguage,
                                                                sessionId,
                                                                cancellationToken);

                    await foreach ( var segmentData in transcribingEvents )
                    {
                        if ( options.AutodetectLanguageOnce )
                        {
                            detectedLanguage = segmentData.Language;
                        }

                        if ( realtimeOptions.ConcatenateSegmentsToPrompt )
                        {
                            promptBuilder.Append(segmentData.Text);
                        }

                        logger.LogDebug(
                                        "Segment recognized: SessionId={SessionId}, Duration={Duration}ms, ProcessingTime={ProcessingTime}ms",
                                        sessionId,
                                        segmentData.Duration.TotalMilliseconds,
                                        transcribeStopwatch.ElapsedMilliseconds);

                        yield return new RealtimeSegmentRecognized(segmentData, sessionId);
                    }
                }

                if ( options.IncludeSpeechRecogizingEvents && recognizingSegment != null )
                {
                    logger.LogDebug("Processing recognizing segment: Duration={Duration}ms",
                                    recognizingSegment.Duration.TotalMilliseconds);

                    var transcribingEvents = TranscribeSegments(
                                                                recognizingSpeechTranscriptorFactory ?? speechTranscriptorFactory,
                                                                source,
                                                                _processedDuration,
                                                                recognizingSegment.StartTime,
                                                                recognizingSegment.Duration,
                                                                promptBuilder,
                                                                detectedLanguage,
                                                                sessionId,
                                                                cancellationToken);

                    await foreach ( var segment in transcribingEvents )
                    {
                        yield return new RealtimeSegmentRecognizing(segment, sessionId);
                    }
                }

                HandleSegmentProcessing(source, ref _processedDuration, lastNonFinalSegment, recognizingSegment, _lastDuration);

                logger.LogTrace("Segment processing completed in {ElapsedTime}ms",
                                segmentStopwatch.ElapsedMilliseconds);
            }

            var finalStopwatch = Stopwatch.StartNew();
            var lastEvents = TranscribeSegments(
                                                speechTranscriptorFactory,
                                                source,
                                                _processedDuration,
                                                TimeSpan.Zero,
                                                source.Duration - _processedDuration,
                                                promptBuilder,
                                                detectedLanguage,
                                                sessionId,
                                                cancellationToken);

            await foreach ( var segmentData in lastEvents )
            {
                if ( realtimeOptions.ConcatenateSegmentsToPrompt )
                {
                    promptBuilder.Append(segmentData.Text);
                }

                yield return new RealtimeSegmentRecognized(segmentData, sessionId);
            }

            logger.LogInformation(
                                  "Transcription session completed: SessionId={SessionId}, TotalDuration={TotalDuration}ms, TotalProcessingTime={TotalTime}ms",
                                  sessionId,
                                  source.Duration.TotalMilliseconds,
                                  stopwatch.ElapsedMilliseconds);

            yield return new RealtimeSessionStopped(sessionId);
        }
        finally
        {
            await CleanupSessionAsync(sessionId);
        }
    }

    private void HandleSegmentProcessing(
        IAudioSource source,
        ref TimeSpan processedDuration,
        VadSegment?  lastNonFinalSegment,
        VadSegment?  recognizingSegment,
        TimeSpan     lastDuration)
    {
        if ( lastNonFinalSegment != null )
        {
            var skippingDuration = lastNonFinalSegment.StartTime + lastNonFinalSegment.Duration;
            processedDuration += skippingDuration;

            if ( source is IDiscardableAudioSource discardableSource )
            {
                var lastSegmentEndFrameIndex = (int)(skippingDuration.TotalMilliseconds * source.SampleRate / 1000d) - 1;
                discardableSource.DiscardFrames(lastSegmentEndFrameIndex);
                logger.LogTrace("Discarded frames up to index {FrameIndex}", lastSegmentEndFrameIndex);
            }
        }
        else if ( recognizingSegment == null )
        {
            if ( lastDuration - processedDuration > realtimeOptions.SilenceDiscardInterval )
            {
                var silenceDurationToDiscard = TimeSpan.FromTicks(realtimeOptions.SilenceDiscardInterval.Ticks / 2);
                processedDuration += silenceDurationToDiscard;

                if ( source is IDiscardableAudioSource discardableSource )
                {
                    var halfSilenceIndex = (int)(silenceDurationToDiscard.TotalMilliseconds * source.SampleRate / 1000d) - 1;
                    discardableSource.DiscardFrames(halfSilenceIndex);
                    logger.LogTrace("Discarded silence frames up to index {FrameIndex}", halfSilenceIndex);
                }
            }
        }
    }

    private async IAsyncEnumerable<TranscriptSegment> TranscribeSegments(
        ISpeechTranscriptorFactory                 transcriptorFactory,
        IAudioSource                               source,
        TimeSpan                                   processedDuration,
        TimeSpan                                   startTime,
        TimeSpan                                   duration,
        StringBuilder                              promptBuilder,
        CultureInfo?                               detectedLanguage,
        string                                     sessionId,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        if ( duration < realtimeOptions.MinTranscriptDuration )
        {
            yield break;
        }

        startTime += processedDuration;
        var paddedStart = startTime - realtimeOptions.PaddingDuration;

        if ( paddedStart < processedDuration )
        {
            paddedStart = processedDuration;
        }

        var paddedDuration = duration + realtimeOptions.PaddingDuration;

        using IAudioSource paddedSource = paddedDuration < realtimeOptions.MinDurationWithPadding
                                              ? GetSilenceAddedSource(source, paddedStart, paddedDuration)
                                              : new SliceAudioSource(source, paddedStart, paddedDuration);

        var languageAutodetect = options.LanguageAutoDetect;
        var language           = options.Language;

        if ( languageAutodetect && options.AutodetectLanguageOnce && detectedLanguage != null )
        {
            languageAutodetect = false;
            language           = detectedLanguage;
        }

        var currentOptions = options with { Prompt = realtimeOptions.ConcatenateSegmentsToPrompt ? promptBuilder.ToString() : options.Prompt, LanguageAutoDetect = languageAutodetect, Language = language };

        var transcriptor = GetOrCreateTranscriptor(transcriptorFactory, currentOptions, sessionId);

        await foreach ( var segment in transcriptor.TranscribeAsync(paddedSource, cancellationToken) )
        {
            segment.StartTime += processedDuration;

            yield return segment;
        }
    }

    private ISpeechTranscriptor GetOrCreateTranscriptor(
        ISpeechTranscriptorFactory        factory,
        RealtimeSpeechTranscriptorOptions currentOptions,
        string                            sessionId)
    {
        var cacheKey = $"{sessionId}_{factory.GetHashCode()}";

        lock (cacheLock)
        {
            if ( !transcriptorCache.TryGetValue(cacheKey, out var transcriptor) )
            {
                transcriptor = factory.Create(currentOptions);
                transcriptorCache.Add(cacheKey, transcriptor);
            }

            return transcriptor;
        }
    }

    private async ValueTask CleanupSessionAsync(string sessionId)
    {
        // lock (cacheLock)
        // {
        var keysToRemove = transcriptorCache.Keys
                                            .Where(k => k.StartsWith($"{sessionId}_"))
                                            .ToList();

        foreach ( var key in keysToRemove )
        {
            if ( !transcriptorCache.TryGetValue(key, out var transcriptor) )
            {
                continue;
            }

            await transcriptor.DisposeAsync();
            transcriptorCache.Remove(key);
        }
        // }
    }

    private ConcatAudioSource GetSilenceAddedSource(IAudioSource source, TimeSpan paddedStart, TimeSpan paddedDuration)
    {
        var silenceDuration = new TimeSpan((realtimeOptions.MinDurationWithPadding.Ticks - paddedDuration.Ticks) / 2);
        var preSilence      = new SilenceAudioSource(silenceDuration, source.SampleRate, source.Metadata, source.ChannelCount, source.BitsPerSample);
        var postSilence     = new SilenceAudioSource(silenceDuration, source.SampleRate, source.Metadata, source.ChannelCount, source.BitsPerSample);

        return new ConcatAudioSource([preSilence, new SliceAudioSource(source, paddedStart, paddedDuration), postSilence], source.Metadata);
    }
}

using System.Buffers;

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

namespace PersonaEngine.Lib.TTS.RVC;

public class CrepeOnnx : IF0Predictor
{
    private const int SAMPLE_RATE = 16000;

    private const int WINDOW_SIZE = 1024;

    private const int PITCH_BINS = 360;

    private const int HOP_LENGTH = SAMPLE_RATE / 100; // 10ms

    private const int BATCH_SIZE = 512;

    private readonly float[] _inputBatchBuffer;

    // Preallocated buffers for zero-allocation processing
    private readonly DenseTensor<float> _inputTensor;

    private readonly float[] _logPInitBuffer;

    // Additional preallocated buffers for Viterbi decoding
    private readonly float[,] _logProbBuffer;

    private readonly float[] _medianBuffer;

    private readonly int[,] _ptrBuffer;

    private readonly InferenceSession _session;

    private readonly int[] _stateBuffer;

    private readonly float[,] _transitionMatrix;

    private readonly float[,] _transOutBuffer;

    private readonly float[,] _valueBuffer;

    private readonly float[] _windowBuffer;

    public CrepeOnnx(string modelPath)
    {
        var options = new SessionOptions {
                                             EnableMemoryPattern    = true,
                                             ExecutionMode          = ExecutionMode.ORT_PARALLEL,
                                             InterOpNumThreads      = Environment.ProcessorCount,
                                             IntraOpNumThreads      = Environment.ProcessorCount,
                                             GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
                                             LogSeverityLevel       = OrtLoggingLevel.ORT_LOGGING_LEVEL_FATAL
                                         };

        options.AppendExecutionProvider_CPU();

        _session = new InferenceSession(modelPath, options);

        // Preallocate buffers
        _inputBatchBuffer = new float[BATCH_SIZE * WINDOW_SIZE];
        _inputTensor      = new DenseTensor<float>(new[] { BATCH_SIZE, WINDOW_SIZE });
        _windowBuffer     = new float[WINDOW_SIZE];
        _medianBuffer     = new float[5]; // For median filtering with window size 5

        // Preallocate Viterbi algorithm buffers
        _logProbBuffer    = new float[BATCH_SIZE, PITCH_BINS];
        _valueBuffer      = new float[BATCH_SIZE, PITCH_BINS];
        _ptrBuffer        = new int[BATCH_SIZE, PITCH_BINS];
        _logPInitBuffer   = new float[PITCH_BINS];
        _transitionMatrix = CreateTransitionMatrix();
        _transOutBuffer   = new float[PITCH_BINS, PITCH_BINS];
        _stateBuffer      = new int[BATCH_SIZE];

        // Initialize _logPInitBuffer with equal probabilities
        for ( var i = 0; i < PITCH_BINS; i++ )
        {
            _logPInitBuffer[i] = (float)Math.Log(1.0f / PITCH_BINS + float.Epsilon);
        }
    }

    public void Dispose() { _session?.Dispose(); }

    public void ComputeF0(ReadOnlyMemory<float> wav, Memory<float> f0Output, int length)
    {
        var wavSpan    = wav.Span;
        var outputSpan = f0Output.Span;

        if ( length > outputSpan.Length )
        {
            throw new ArgumentException("Output buffer is too small", nameof(f0Output));
        }

        // Preallocate periodicity data buffer
        var pdBuffer = ArrayPool<float>.Shared.Rent(length);
        try
        {
            var pdSpan = new Span<float>(pdBuffer, 0, length);

            // Process the audio to extract F0
            Crepe(wavSpan, outputSpan.Slice(0, length), pdSpan);

            // Apply post-processing
            for ( var i = 0; i < length; i++ )
            {
                if ( pdSpan[i] < 0.1 )
                {
                    outputSpan[i] = 0;
                }
            }
        }
        finally
        {
            ArrayPool<float>.Shared.Return(pdBuffer);
        }
    }

    private void Crepe(ReadOnlySpan<float> x, Span<float> f0, Span<float> pd)
    {
        var total_frames = 1 + x.Length / HOP_LENGTH;

        for ( var b = 0; b < total_frames; b += BATCH_SIZE )
        {
            var currentBatchSize = Math.Min(BATCH_SIZE, total_frames - b);

            // Fill the batch input buffer
            FillBatch(x, b, currentBatchSize);

            // Run inference on the batch
            var probabilities = Run(currentBatchSize);

            // Decode the probabilities into F0 values
            Decode(probabilities, f0, pd, currentBatchSize, b);
        }

        // Apply post-processing
        MedianFilter(pd, 3);
        MeanFilter(f0, 3);
    }

    private void FillBatch(ReadOnlySpan<float> x, int batchOffset, int batchSize)
    {
        for ( var i = 0; i < batchSize; i++ )
        {
            var frameIndex  = batchOffset + i;
            var inputOffset = i * WINDOW_SIZE;
            FillFrame(x, _inputBatchBuffer.AsSpan(inputOffset, WINDOW_SIZE), frameIndex);
        }
    }

    private void FillFrame(ReadOnlySpan<float> x, Span<float> frame, int frameIndex)
    {
        var pad   = WINDOW_SIZE / 2;
        var start = frameIndex * HOP_LENGTH - pad;

        for ( var j = 0; j < WINDOW_SIZE; j++ )
        {
            var   k = start + j;
            float v = 0;

            if ( k < 0 )
            {
                // Reflection
                k = -k;
            }

            if ( k >= x.Length )
            {
                // Reflection
                k = x.Length - 1 - (k - x.Length);
            }

            if ( k >= 0 && k < x.Length )
            {
                v = x[k];
            }

            frame[j] = v;
        }

        // Normalize the frame
        Normalize(frame);
    }

    private void Normalize(Span<float> input)
    {
        // Mean center and scale
        float sum = 0;
        for ( var j = 0; j < input.Length; j++ )
        {
            sum += input[j];
        }

        var   mean     = sum / input.Length;
        float stdValue = 0;

        for ( var j = 0; j < input.Length; j++ )
        {
            input[j] =  input[j] - mean;
            stdValue += input[j] * input[j];
        }

        stdValue = stdValue / input.Length;
        stdValue = (float)Math.Sqrt(stdValue);

        if ( stdValue < 1e-10 )
        {
            stdValue = 1e-10f;
        }

        for ( var j = 0; j < input.Length; j++ )
        {
            input[j] = input[j] / stdValue;
        }
    }

    private float[] Run(int batchSize)
    {
        // Copy the batch data to the input tensor
        for ( var i = 0; i < batchSize; i++ )
        {
            for ( var j = 0; j < WINDOW_SIZE; j++ )
            {
                _inputTensor[i, j] = _inputBatchBuffer[i * WINDOW_SIZE + j];
            }
        }

        var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", _inputTensor) };

        using var results = _session.Run(inputs);

        return results[0].AsTensor<float>().ToArray();
    }

    private void Decode(float[] probabilities, Span<float> f0, Span<float> pd, int outputSize, int offset)
    {
        // Remove frequencies outside of allowable range
        const int minidx = 39;  // 50hz
        const int maxidx = 308; // 2006hz

        for ( var t = 0; t < outputSize; t++ )
        {
            for ( var i = 0; i < PITCH_BINS; i++ )
            {
                if ( i < minidx || i >= maxidx )
                {
                    probabilities[t * PITCH_BINS + i] = float.NegativeInfinity;
                }
            }
        }

        // Use Viterbi algorithm to decode the probabilities
        DecodeViterbi(probabilities, f0, pd, outputSize, offset);
    }

    private void DecodeViterbi(float[] probabilities, Span<float> f0, Span<float> pd, int nSteps, int offset)
    {
        // Transfer probabilities to log_prob buffer and apply softmax
        for ( var i = 0; i < nSteps * PITCH_BINS; i++ )
        {
            _logProbBuffer[i / PITCH_BINS, i % PITCH_BINS] = probabilities[i];
        }

        Softmax(_logProbBuffer, nSteps);

        // Apply log to probabilities
        for ( var y = 0; y < nSteps; y++ )
        {
            for ( var x = 0; x < PITCH_BINS; x++ )
            {
                _logProbBuffer[y, x] = (float)Math.Log(_logProbBuffer[y, x] + float.Epsilon);
            }
        }

        // Initialize first step values
        for ( var i = 0; i < PITCH_BINS; i++ )
        {
            _valueBuffer[0, i] = _logProbBuffer[0, i] + _logPInitBuffer[i];
        }

        // Viterbi algorithm
        for ( var t = 1; t < nSteps; t++ )
        {
            // Calculate transition outputs
            for ( var y = 0; y < PITCH_BINS; y++ )
            {
                for ( var x = 0; x < PITCH_BINS; x++ )
                {
                    _transOutBuffer[y, x] = _valueBuffer[t - 1, x] + _transitionMatrix[x, y]; // Transposed matrix
                }
            }

            for ( var j = 0; j < PITCH_BINS; j++ )
            {
                // Find argmax
                var maxI    = 0;
                var maxProb = float.NegativeInfinity;
                for ( var k = 0; k < PITCH_BINS; k++ )
                {
                    if ( maxProb < _transOutBuffer[j, k] )
                    {
                        maxProb = _transOutBuffer[j, k];
                        maxI    = k;
                    }
                }

                _ptrBuffer[t, j]   = maxI;
                _valueBuffer[t, j] = _logProbBuffer[t, j] + _transOutBuffer[j, _ptrBuffer[t, j]];
            }
        }

        // Find the most likely final state
        var maxI2    = 0;
        var maxProb2 = float.NegativeInfinity;
        for ( var k = 0; k < PITCH_BINS; k++ )
        {
            if ( maxProb2 < _valueBuffer[nSteps - 1, k] )
            {
                maxProb2 = _valueBuffer[nSteps - 1, k];
                maxI2    = k;
            }
        }

        // Backward pass to find optimal path
        _stateBuffer[nSteps - 1] = maxI2;
        for ( var t = nSteps - 2; t >= 0; t-- )
        {
            _stateBuffer[t] = _ptrBuffer[t + 1, _stateBuffer[t + 1]];
        }

        // Convert to f0 values
        for ( var t = 0; t < nSteps; t++ )
        {
            var bins        = _stateBuffer[t];
            var periodicity = Periodicity(probabilities, t, bins);
            var frequency   = ConvertToFrequency(bins);

            if ( offset + t < f0.Length )
            {
                f0[offset + t] = frequency;
                pd[offset + t] = periodicity;
            }
        }
    }

    private void Softmax(float[,] data, int nSteps)
    {
        for ( var t = 0; t < nSteps; t++ )
        {
            float sum = 0;
            for ( var i = 0; i < PITCH_BINS; i++ )
            {
                sum += (float)Math.Exp(data[t, i]);
            }

            for ( var i = 0; i < PITCH_BINS; i++ )
            {
                data[t, i] = (float)Math.Exp(data[t, i]) / sum;
            }
        }
    }

    private float[,] CreateTransitionMatrix()
    {
        var transition = new float[PITCH_BINS, PITCH_BINS];
        for ( var y = 0; y < PITCH_BINS; y++ )
        {
            float sum = 0;
            for ( var x = 0; x < PITCH_BINS; x++ )
            {
                var v = 12 - Math.Abs(x - y);
                if ( v < 0 )
                {
                    v = 0;
                }

                transition[y, x] =  v;
                sum              += v;
            }

            for ( var x = 0; x < PITCH_BINS; x++ )
            {
                transition[y, x] = transition[y, x] / sum;

                // Pre-apply log to the transition matrix for efficiency
                transition[y, x] = (float)Math.Log(transition[y, x] + float.Epsilon);
            }
        }

        return transition;
    }

    private float Periodicity(float[] probabilities, int t, int bins) { return probabilities[t * PITCH_BINS + bins]; }

    private float ConvertToFrequency(int bin)
    {
        float CENTS_PER_BIN = 20;
        var   cents         = CENTS_PER_BIN * bin + 1997.3794084376191f;
        var   frequency     = 10 * (float)Math.Pow(2, cents / 1200);

        return frequency;
    }

    private void MedianFilter(Span<float> data, int windowSize)
    {
        if ( windowSize > _medianBuffer.Length || windowSize % 2 == 0 )
        {
            throw new ArgumentException("Window size must be odd and <= buffer size", nameof(windowSize));
        }

        // Create a copy of the original data
        var original = ArrayPool<float>.Shared.Rent(data.Length);
        try
        {
            data.CopyTo(original);

            for ( var i = 0; i < data.Length; i++ )
            {
                // Fill the window buffer
                for ( var j = 0; j < windowSize; j++ )
                {
                    var k = i + j - windowSize / 2;

                    // Handle boundary conditions
                    if ( k < 0 )
                    {
                        k = 0;
                    }

                    if ( k >= data.Length )
                    {
                        k = data.Length - 1;
                    }

                    _medianBuffer[j] = original[k];
                }

                // Sort the window
                Array.Sort(_medianBuffer, 0, windowSize);

                // Set the median value
                data[i] = _medianBuffer[windowSize / 2];
            }
        }
        finally
        {
            ArrayPool<float>.Shared.Return(original);
        }
    }

    private void MeanFilter(Span<float> data, int windowSize)
    {
        // Create a copy of the original data
        var original = ArrayPool<float>.Shared.Rent(data.Length);
        try
        {
            data.CopyTo(original);

            for ( var i = 0; i < data.Length; i++ )
            {
                float sum = 0;

                for ( var j = 0; j < windowSize; j++ )
                {
                    var k = i + j - windowSize / 2;

                    // Handle boundary conditions
                    if ( k < 0 )
                    {
                        k = 0;
                    }

                    if ( k >= data.Length )
                    {
                        k = data.Length - 1;
                    }

                    sum += original[k];
                }

                // Set the mean value
                data[i] = sum / windowSize;
            }
        }
        finally
        {
            ArrayPool<float>.Shared.Return(original);
        }
    }
}

using System.Buffers;
using System.Runtime.CompilerServices;

using PersonaEngine.Lib.Audio;
using PersonaEngine.Lib.Utils;

namespace PersonaEngine.Lib.ASR.VAD;

internal class SileroVadDetector : IVadDetector
{
    private readonly int _minSilenceSamples;

    private readonly int _minSpeechSamples;

    private readonly SileroVadOnnxModel _model;

    private readonly float _negThreshold;

    private readonly float _threshold;

    public SileroVadDetector(VadDetectorOptions vadDetectorOptions, SileroVadOptions sileroVadOptions)
    {
        _model             = new SileroVadOnnxModel(sileroVadOptions.ModelPath);
        _threshold         = sileroVadOptions.Threshold;
        _negThreshold      = _threshold - sileroVadOptions.ThresholdGap;
        _minSpeechSamples  = (int)(16d * vadDetectorOptions.MinSpeechDuration.TotalMilliseconds);
        _minSilenceSamples = (int)(16d * vadDetectorOptions.MinSilenceDuration.TotalMilliseconds);
    }

    public async IAsyncEnumerable<VadSegment> DetectSegmentsAsync(IAudioSource source, [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        ValidateSource(source);

        foreach ( var segment in DetectSegments(await source.GetSamplesAsync(0, cancellationToken: cancellationToken)) )
        {
            cancellationToken.ThrowIfCancellationRequested();

            yield return segment;
        }
    }

    private IEnumerable<VadSegment> DetectSegments(Memory<float> samples)
    {
        var  state                = _model.CreateInferenceState();
        int? startingIndex        = null;
        int? startingSilenceIndex = null;

        for ( var i = 0; i < samples.Length - SileroConstants.BatchSize; i += SileroConstants.BatchSize )
        {
            Memory<float> slice;
            float[]?      rentedMemory = null;
            try
            {
                if ( i == 0 )
                {
                    // the first batch will have the empty context (which we need to copy at the beggining of the slice)
                    rentedMemory = ArrayPool<float>.Shared.Rent(SileroConstants.ContextSize + SileroConstants.BatchSize);
                    Array.Clear(rentedMemory, 0, SileroConstants.ContextSize);
                    samples.Span.Slice(0, SileroConstants.BatchSize).CopyTo(rentedMemory.AsSpan(SileroConstants.ContextSize));
                    slice = rentedMemory.AsMemory(0, SileroConstants.BatchSize + SileroConstants.ContextSize);
                }
                else
                {
                    slice = samples.Slice(i - SileroConstants.ContextSize, SileroConstants.BatchSize + SileroConstants.ContextSize);
                }

                var prob = _model.Call(slice, state);
                if ( !startingIndex.HasValue )
                {
                    if ( prob > _threshold )
                    {
                        startingIndex = i;
                    }

                    continue;
                }

                // We are in speech
                if ( prob > _threshold )
                {
                    startingSilenceIndex = null;

                    continue;
                }

                if ( prob > _negThreshold )
                {
                    // We are still in speech and the current batch is between the threshold and the negative threshold
                    // We continue to the next batch
                    continue;
                }

                if ( startingSilenceIndex == null )
                {
                    startingSilenceIndex = i;

                    continue;
                }

                var silenceLength = i - startingSilenceIndex.Value;

                if ( silenceLength > _minSilenceSamples )
                {
                    // We have silence after speech exceeding the minimum silence duration
                    var length = i - startingIndex.Value;
                    if ( length >= _minSpeechSamples )
                    {
                        yield return new VadSegment { StartTime = TimeSpan.FromMilliseconds(startingIndex.Value * 1000d / SileroConstants.SampleRate), Duration = TimeSpan.FromMilliseconds(length * 1000d / SileroConstants.SampleRate) };
                    }

                    startingIndex        = null;
                    startingSilenceIndex = null;
                }
            }
            finally
            {
                if ( rentedMemory != null )
                {
                    ArrayPool<float>.Shared.Return(rentedMemory, ArrayPoolConfig.ClearOnReturn);
                }
            }
        }

        // The last segment if it was already started (might be incomplete for sources that are not yet finished)
        if ( startingIndex.HasValue )
        {
            var length = samples.Length - startingIndex.Value;
            if ( length >= _minSpeechSamples )
            {
                yield return new VadSegment { StartTime = TimeSpan.FromMilliseconds(startingIndex.Value * 1000d / SileroConstants.SampleRate), Duration = TimeSpan.FromMilliseconds(length * 1000d / SileroConstants.SampleRate), IsIncomplete = true };
            }
        }
    }

    private static void ValidateSource(IAudioSource source)
    {
        if ( source.ChannelCount != 1 )
        {
            throw new NotSupportedException("Only mono-channel audio is supported. Consider one channel aggregation on the audio source.");
        }

        if ( source.SampleRate != 16000 )
        {
            throw new NotSupportedException("Only 16 kHz audio is supported. Consider resampling before calling this transcriptor.");
        }
    }
}

using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

namespace PersonaEngine.Lib.TTS.RVC;

public class CrepeOnnxSimd : IF0Predictor, IDisposable
{
    private const int SAMPLE_RATE = 16000;

    private const int WINDOW_SIZE = 1024;

    private const int PITCH_BINS = 360;

    private const int HOP_LENGTH = SAMPLE_RATE / 100; // 10ms

    private const int BATCH_SIZE = 512;

    // Preallocated buffers
    private readonly float[] _inputBatchBuffer;

    private readonly DenseTensor<float> _inputTensor;

    private readonly float[] _logPInitBuffer;

    // Flattened arrays for better cache locality and SIMD operations
    private readonly float[] _logProbBuffer;

    private readonly float[] _medianBuffer;

    private readonly int[] _ptrBuffer;

    private readonly InferenceSession _session;

    private readonly int[] _stateBuffer;

    private readonly float[] _tempBuffer; // Used for temporary calculations

    private readonly float[] _transitionMatrix;

    private readonly float[] _transOutBuffer;

    private readonly float[] _valueBuffer;

    // SIMD vector size based on hardware
    private readonly int _vectorSize;

    public CrepeOnnxSimd(string modelPath)
    {
        // Determine vector size based on hardware capabilities
        _vectorSize = Vector<float>.Count;

        var options = new SessionOptions {
                                             EnableMemoryPattern    = true,
                                             ExecutionMode          = ExecutionMode.ORT_PARALLEL,
                                             InterOpNumThreads      = Environment.ProcessorCount,
                                             IntraOpNumThreads      = Environment.ProcessorCount,
                                             GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
                                             LogSeverityLevel       = OrtLoggingLevel.ORT_LOGGING_LEVEL_FATAL
                                         };

        // Use hardware specific optimizations if available
        options.AppendExecutionProvider_CPU();

        _session = new InferenceSession(modelPath, options);

        // Initialize preallocated buffers
        _inputBatchBuffer = new float[BATCH_SIZE * WINDOW_SIZE];
        _inputTensor      = new DenseTensor<float>(new[] { BATCH_SIZE, WINDOW_SIZE });
        _medianBuffer     = new float[5]; // For median filtering with window size 5

        // Initialize flattened buffers for Viterbi algorithm
        _logProbBuffer    = new float[BATCH_SIZE * PITCH_BINS];
        _valueBuffer      = new float[BATCH_SIZE * PITCH_BINS];
        _ptrBuffer        = new int[BATCH_SIZE * PITCH_BINS];
        _logPInitBuffer   = new float[PITCH_BINS];
        _transitionMatrix = new float[PITCH_BINS * PITCH_BINS];
        _transOutBuffer   = new float[PITCH_BINS * PITCH_BINS];
        _stateBuffer      = new int[BATCH_SIZE];
        _tempBuffer       = new float[Math.Max(BATCH_SIZE, PITCH_BINS) * _vectorSize];

        // Initialize logPInitBuffer with equal probabilities
        var logInitProb = (float)Math.Log(1.0f / PITCH_BINS + float.Epsilon);
        FillArray(_logPInitBuffer, logInitProb);

        // Initialize transition matrix
        InitializeTransitionMatrix(_transitionMatrix);
    }

    public void Dispose() { _session?.Dispose(); }

    public void ComputeF0(ReadOnlyMemory<float> wav, Memory<float> f0Output, int length)
    {
        if ( length > f0Output.Length )
        {
            throw new ArgumentException("Output buffer is too small", nameof(f0Output));
        }

        // Rent buffer from pool for periodicity data
        var pdBuffer = ArrayPool<float>.Shared.Rent(length);
        try
        {
            var pdSpan  = pdBuffer.AsSpan(0, length);
            var wavSpan = wav.Span;
            var f0Span  = f0Output.Span.Slice(0, length);

            // Process audio to extract F0
            Crepe(wavSpan, f0Span, pdSpan);

            // Apply post-processing with SIMD
            ApplyPeriodicityThreshold(f0Span, pdSpan, length);
        }
        finally
        {
            ArrayPool<float>.Shared.Return(pdBuffer);
        }
    }

    private void Crepe(ReadOnlySpan<float> x, Span<float> f0, Span<float> pd)
    {
        var totalFrames = 1 + x.Length / HOP_LENGTH;

        for ( var b = 0; b < totalFrames; b += BATCH_SIZE )
        {
            var currentBatchSize = Math.Min(BATCH_SIZE, totalFrames - b);

            // Fill the batch input buffer with SIMD
            FillBatch(x, b, currentBatchSize);

            // Run inference on the batch
            var probabilities = Run(currentBatchSize);

            // Decode the probabilities into F0 values
            Decode(probabilities, f0, pd, currentBatchSize, b);
        }

        // Apply post-processing with SIMD
        MedianFilter(pd, 3);
        MeanFilter(f0, 3);
    }

    private void FillBatch(ReadOnlySpan<float> x, int batchOffset, int batchSize)
    {
        for ( var i = 0; i < batchSize; i++ )
        {
            var frameIndex  = batchOffset + i;
            var inputOffset = i * WINDOW_SIZE;
            FillFrame(x, _inputBatchBuffer.AsSpan(inputOffset, WINDOW_SIZE), frameIndex);
        }
    }

    private void FillFrame(ReadOnlySpan<float> x, Span<float> frame, int frameIndex)
    {
        var pad   = WINDOW_SIZE / 2;
        var start = frameIndex * HOP_LENGTH - pad;

        for ( var j = 0; j < WINDOW_SIZE; j++ )
        {
            var   k = start + j;
            float v = 0;

            if ( k < 0 )
            {
                // Reflection padding
                k = -k;
            }

            if ( k >= x.Length )
            {
                // Reflection padding
                k = x.Length - 1 - (k - x.Length);
            }

            if ( k >= 0 && k < x.Length )
            {
                v = x[k];
            }

            frame[j] = v;
        }

        // Normalize the frame using SIMD
        NormalizeAvx(frame);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void NormalizeSimd(Span<float> input)
    {
        // 1. Calculate mean using SIMD
        float sum         = 0;
        var   vectorCount = input.Length / _vectorSize;

        for ( var i = 0; i < vectorCount; i++ )
        {
            var v = new Vector<float>(input.Slice(i * _vectorSize, _vectorSize));
            sum += Vector.Sum(v);
        }

        // Handle remainder
        for ( var i = vectorCount * _vectorSize; i < input.Length; i++ )
        {
            sum += input[i];
        }

        var mean = sum / input.Length;

        // 2. Subtract mean and calculate variance using SIMD
        float variance   = 0;
        var   meanVector = new Vector<float>(mean);

        for ( var i = 0; i < vectorCount; i++ )
        {
            var slice    = input.Slice(i * _vectorSize, _vectorSize);
            var v        = new Vector<float>(slice);
            var centered = v - meanVector;
            centered.CopyTo(slice);
            variance += Vector.Sum(centered * centered);
        }

        // Handle remainder
        for ( var i = vectorCount * _vectorSize; i < input.Length; i++ )
        {
            input[i] -= mean;
            variance += input[i] * input[i];
        }

        // 3. Calculate stddev and normalize
        var stddev = MathF.Sqrt(variance / input.Length);
        stddev = Math.Max(stddev, 1e-10f);

        var stddevVector = new Vector<float>(stddev);

        for ( var i = 0; i < vectorCount; i++ )
        {
            var slice      = input.Slice(i * _vectorSize, _vectorSize);
            var v          = new Vector<float>(slice);
            var normalized = v / stddevVector;
            normalized.CopyTo(slice);
        }

        // Handle remainder
        for ( var i = vectorCount * _vectorSize; i < input.Length; i++ )
        {
            input[i] /= stddev;
        }
    }

    // Hardware-specific optimization for AVX2-capable systems
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void NormalizeAvx(Span<float> input)
    {
        if ( !Avx.IsSupported || input.Length < 8 )
        {
            NormalizeSimd(input);

            return;
        }

        unsafe
        {
            fixed (float* pInput = input)
            {
                // Calculate sum using AVX
                var sumVec         = Vector256<float>.Zero;
                var avxVectorCount = input.Length / 8;

                for ( var i = 0; i < avxVectorCount; i++ )
                {
                    var v = Avx.LoadVector256(pInput + i * 8);
                    sumVec = Avx.Add(sumVec, v);
                }

                // Horizontal sum
                var sumArray = stackalloc float[8];
                Avx.Store(sumArray, sumVec);

                float sum = 0;
                for ( var i = 0; i < 8; i++ )
                {
                    sum += sumArray[i];
                }

                // Handle remainder
                for ( var i = avxVectorCount * 8; i < input.Length; i++ )
                {
                    sum += pInput[i];
                }

                var mean    = sum / input.Length;
                var meanVec = Vector256.Create(mean);

                // Subtract mean and compute variance
                var varianceVec = Vector256<float>.Zero;

                for ( var i = 0; i < avxVectorCount; i++ )
                {
                    var v        = Avx.LoadVector256(pInput + i * 8);
                    var centered = Avx.Subtract(v, meanVec);
                    Avx.Store(pInput + i * 8, centered);
                    varianceVec = Avx.Add(varianceVec, Avx.Multiply(centered, centered));
                }

                // Get variance
                var varArray = stackalloc float[8];
                Avx.Store(varArray, varianceVec);

                float variance = 0;
                for ( var i = 0; i < 8; i++ )
                {
                    variance += varArray[i];
                }

                // Handle remainder
                for ( var i = avxVectorCount * 8; i < input.Length; i++ )
                {
                    pInput[i] -= mean;
                    variance  += pInput[i] * pInput[i];
                }

                var stddev = MathF.Sqrt(variance / input.Length);
                stddev = Math.Max(stddev, 1e-10f);

                var stddevVec = Vector256.Create(stddev);

                // Normalize
                for ( var i = 0; i < avxVectorCount; i++ )
                {
                    var v          = Avx.LoadVector256(pInput + i * 8);
                    var normalized = Avx.Divide(v, stddevVec);
                    Avx.Store(pInput + i * 8, normalized);
                }

                // Handle remainder
                for ( var i = avxVectorCount * 8; i < input.Length; i++ )
                {
                    pInput[i] /= stddev;
                }
            }
        }
    }

    private float[] Run(int batchSize)
    {
        // Copy the batch data to the input tensor using SIMD where possible
        for ( var i = 0; i < batchSize; i++ )
        {
            var inputOffset = i * WINDOW_SIZE;
            var vectorCount = WINDOW_SIZE / _vectorSize;

            for ( var v = 0; v < vectorCount; v++ )
            {
                var vectorOffset = v * _vectorSize;
                var source       = new Vector<float>(_inputBatchBuffer.AsSpan(inputOffset + vectorOffset, _vectorSize));

                for ( var j = 0; j < _vectorSize; j++ )
                {
                    _inputTensor[i, vectorOffset + j] = source[j];
                }
            }

            // Handle remainder
            for ( var j = vectorCount * _vectorSize; j < WINDOW_SIZE; j++ )
            {
                _inputTensor[i, j] = _inputBatchBuffer[inputOffset + j];
            }
        }

        var       inputs  = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", _inputTensor) };
        using var results = _session.Run(inputs);

        return results[0].AsTensor<float>().ToArray();
    }

    private void Decode(float[] probabilities, Span<float> f0, Span<float> pd, int outputSize, int offset)
    {
        // Apply frequency range limitation using SIMD where possible
        const int minidx = 39;  // 50hz
        const int maxidx = 308; // 2006hz

        ApplyFrequencyRangeLimit(probabilities, outputSize, minidx, maxidx);

        // Make a safe copy of the spans since we can't capture them in parallel operations
        var f0Array = new float[f0.Length];
        var pdArray = new float[pd.Length];
        f0.CopyTo(f0Array);
        pd.CopyTo(pdArray);

        // Use Viterbi algorithm to decode the probabilities
        DecodeViterbi(probabilities, f0Array, pdArray, outputSize, offset);

        // Copy results back
        for ( var i = 0; i < outputSize; i++ )
        {
            if ( offset + i < f0.Length )
            {
                f0[offset + i] = f0Array[offset + i];
                pd[offset + i] = pdArray[offset + i];
            }
        }
    }

    private void ApplyFrequencyRangeLimit(float[] probabilities, int outputSize, int minIdx, int maxIdx)
    {
        // Set values outside the frequency range to negative infinity
        Parallel.For(0, outputSize, t =>
                                    {
                                        var baseIdx = t * PITCH_BINS;

                                        // Handle first part (below minIdx)
                                        for ( var i = 0; i < minIdx; i++ )
                                        {
                                            probabilities[baseIdx + i] = float.NegativeInfinity;
                                        }

                                        // Handle second part (above maxIdx)
                                        for ( var i = maxIdx; i < PITCH_BINS; i++ )
                                        {
                                            probabilities[baseIdx + i] = float.NegativeInfinity;
                                        }
                                    });
    }

    private void DecodeViterbi(float[] probabilities, float[] f0, float[] pd, int nSteps, int offset)
    {
        // Transfer probabilities to logProbBuffer and apply softmax
        Buffer.BlockCopy(probabilities, 0, _logProbBuffer, 0, nSteps * PITCH_BINS * sizeof(float));

        // Apply softmax with SIMD
        SoftmaxSimd(_logProbBuffer, nSteps);

        // Apply log to probabilities
        ApplyLogToProbs(_logProbBuffer, nSteps);

        // Initialize first step values
        for ( var i = 0; i < PITCH_BINS; i++ )
        {
            _valueBuffer[i] = _logProbBuffer[i] + _logPInitBuffer[i];
        }

        // Viterbi algorithm (forward pass)
        for ( var t = 1; t < nSteps; t++ )
        {
            ViterbiForward(t, nSteps);
        }

        // Find the most likely final state
        var maxI = FindArgMax(_valueBuffer, (nSteps - 1) * PITCH_BINS, PITCH_BINS);

        // Backward pass to find optimal path
        _stateBuffer[nSteps - 1] = maxI;
        for ( var t = nSteps - 2; t >= 0; t-- )
        {
            _stateBuffer[t] = _ptrBuffer[(t + 1) * PITCH_BINS + _stateBuffer[t + 1]];
        }

        // Convert to f0 values and apply periodicity
        ConvertToF0(probabilities, f0, pd, nSteps, offset);
    }

    private void SoftmaxSimd(float[] data, int nSteps)
    {
        Parallel.For(0, nSteps, t =>
                                {
                                    var baseIdx = t * PITCH_BINS;

                                    // Find max for numerical stability
                                    var max = float.NegativeInfinity;
                                    for ( var i = 0; i < PITCH_BINS; i++ )
                                    {
                                        max = Math.Max(max, data[baseIdx + i]);
                                    }

                                    // Compute exp(x - max) and sum
                                    float sum = 0;
                                    for ( var i = 0; i < PITCH_BINS; i++ )
                                    {
                                        data[baseIdx + i] =  MathF.Exp(data[baseIdx + i] - max);
                                        sum               += data[baseIdx + i];
                                    }

                                    // Normalize
                                    var invSum    = 1.0f / sum;
                                    var vecCount  = PITCH_BINS / _vectorSize;
                                    var invSumVec = new Vector<float>(invSum);

                                    for ( var v = 0; v < vecCount; v++ )
                                    {
                                        var idx        = baseIdx + v * _vectorSize;
                                        var values     = new Vector<float>(data.AsSpan(idx, _vectorSize));
                                        var normalized = values * invSumVec;

                                        for ( var j = 0; j < _vectorSize; j++ )
                                        {
                                            data[idx + j] = normalized[j];
                                        }
                                    }

                                    // Handle remainder
                                    for ( var i = baseIdx + vecCount * _vectorSize; i < baseIdx + PITCH_BINS; i++ )
                                    {
                                        data[i] *= invSum;
                                    }
                                });
    }

    private void ApplyLogToProbs(float[] data, int nSteps)
    {
        var totalSize = nSteps * PITCH_BINS;
        var vecCount  = totalSize / _vectorSize;
        var dataSpan  = data.AsSpan(0, totalSize);

        for ( var v = 0; v < vecCount; v++ )
        {
            var slice     = dataSpan.Slice(v * _vectorSize, _vectorSize);
            var values    = new Vector<float>(slice);
            var logValues = Vector.Log(values + new Vector<float>(float.Epsilon));
            logValues.CopyTo(slice);
        }

        // Handle remainder
        for ( var i = vecCount * _vectorSize; i < totalSize; i++ )
        {
            data[i] = MathF.Log(data[i] + float.Epsilon);
        }
    }

    private void ViterbiForward(int t, int nSteps)
    {
        var baseIdxCurrent = t * PITCH_BINS;
        var baseIdxPrev    = (t - 1) * PITCH_BINS;

        // Fixed number of threads to avoid thread contention
        var threadCount = Math.Min(Environment.ProcessorCount, PITCH_BINS);

        Parallel.For(0, threadCount, threadIdx =>
                                     {
                                         // Each thread processes a chunk of states
                                         var statesPerThread = (PITCH_BINS + threadCount - 1) / threadCount;
                                         var startState      = threadIdx * statesPerThread;
                                         var endState        = Math.Min(startState + statesPerThread, PITCH_BINS);

                                         for ( var j = startState; j < endState; j++ )
                                         {
                                             var maxI   = 0;
                                             var maxVal = float.NegativeInfinity;

                                             // Find max transition - this could be further vectorized for specific hardware
                                             for ( var k = 0; k < PITCH_BINS; k++ )
                                             {
                                                 var transVal = _valueBuffer[baseIdxPrev + k] + _transitionMatrix[j * PITCH_BINS + k];
                                                 if ( transVal > maxVal )
                                                 {
                                                     maxVal = transVal;
                                                     maxI   = k;
                                                 }
                                             }

                                             _ptrBuffer[baseIdxCurrent + j]   = maxI;
                                             _valueBuffer[baseIdxCurrent + j] = _logProbBuffer[baseIdxCurrent + j] + maxVal;
                                         }
                                     });
    }

    private int FindArgMax(float[] data, int offset, int length)
    {
        var maxIdx = 0;
        var maxVal = float.NegativeInfinity;

        for ( var i = 0; i < length; i++ )
        {
            if ( data[offset + i] > maxVal )
            {
                maxVal = data[offset + i];
                maxIdx = i;
            }
        }

        return maxIdx;
    }

    private void ConvertToF0(float[] probabilities, float[] f0, float[] pd, int nSteps, int offset)
    {
        for ( var t = 0; t < nSteps; t++ )
        {
            if ( offset + t >= f0.Length )
            {
                break;
            }

            var bin         = _stateBuffer[t];
            var periodicity = probabilities[t * PITCH_BINS + bin];
            var frequency   = ConvertBinToFrequency(bin);

            f0[offset + t] = frequency;
            pd[offset + t] = periodicity;
        }
    }

    private void ApplyPeriodicityThreshold(Span<float> f0, Span<float> pd, int length)
    {
        const float threshold = 0.1f;
        var         vecCount  = length / _vectorSize;

        // Use SIMD for bulk processing
        for ( var v = 0; v < vecCount; v++ )
        {
            var offset  = v * _vectorSize;
            var pdSlice = pd.Slice(offset, _vectorSize);
            var f0Slice = f0.Slice(offset, _vectorSize);

            var pdVec        = new Vector<float>(pdSlice);
            var thresholdVec = new Vector<float>(threshold);
            var zeroVec      = new Vector<float>(0.0f);
            var f0Vec        = new Vector<float>(f0Slice);

            // Where pd < threshold, set f0 to 0
            var mask   = Vector.LessThan(pdVec, thresholdVec);
            var result = Vector.ConditionalSelect(mask, zeroVec, f0Vec);

            result.CopyTo(f0Slice);
        }

        // Handle remainder with scalar code
        for ( var i = vecCount * _vectorSize; i < length; i++ )
        {
            if ( pd[i] < threshold )
            {
                f0[i] = 0;
            }
        }
    }

    private void MedianFilter(Span<float> data, int windowSize)
    {
        if ( windowSize > _medianBuffer.Length || windowSize % 2 == 0 )
        {
            throw new ArgumentException("Window size must be odd and <= buffer size", nameof(windowSize));
        }

        var original = ArrayPool<float>.Shared.Rent(data.Length);
        var result   = ArrayPool<float>.Shared.Rent(data.Length);
        try
        {
            data.CopyTo(original.AsSpan(0, data.Length));
            var radius = windowSize / 2;
            var length = data.Length;

            // Use thread-local window buffers for parallel processing
            Parallel.For(0, length, i =>
                                    {
                                        // Allocate a local median buffer for each thread
                                        var localMedianBuffer = new float[windowSize];

                                        // Get window values
                                        for ( var j = 0; j < windowSize; j++ )
                                        {
                                            var k = i + j - radius;
                                            k                    = Math.Clamp(k, 0, length - 1);
                                            localMedianBuffer[j] = original[k];
                                        }

                                        // Simple sort for small window
                                        Array.Sort(localMedianBuffer, 0, windowSize);
                                        result[i] = localMedianBuffer[radius];
                                    });

            // Copy results back to the span
            new Span<float>(result, 0, data.Length).CopyTo(data);
        }
        finally
        {
            ArrayPool<float>.Shared.Return(original);
            ArrayPool<float>.Shared.Return(result);
        }
    }

    private void MeanFilter(Span<float> data, int windowSize)
    {
        var original = ArrayPool<float>.Shared.Rent(data.Length);
        var result   = ArrayPool<float>.Shared.Rent(data.Length);
        try
        {
            // Copy to array for processing
            data.CopyTo(original.AsSpan(0, data.Length));
            var radius = windowSize / 2;
            var length = data.Length;

            // Use arrays instead of spans for parallel processing
            Parallel.For(0, length, i =>
                                    {
                                        float sum = 0;
                                        for ( var j = -radius; j <= radius; j++ )
                                        {
                                            var k = Math.Clamp(i + j, 0, length - 1);
                                            sum += original[k];
                                        }

                                        result[i] = sum / windowSize;
                                    });

            // Copy back to span
            new Span<float>(result, 0, data.Length).CopyTo(data);
        }
        finally
        {
            ArrayPool<float>.Shared.Return(original);
            ArrayPool<float>.Shared.Return(result);
        }
    }

    private void InitializeTransitionMatrix(float[] transitionMatrix)
    {
        Parallel.For(0, PITCH_BINS, y =>
                                    {
                                        float sum = 0;
                                        for ( var x = 0; x < PITCH_BINS; x++ )
                                        {
                                            float v = 12 - Math.Abs(x - y);
                                            v                                    =  Math.Max(v, 0);
                                            transitionMatrix[y * PITCH_BINS + x] =  v;
                                            sum                                  += v;
                                        }

                                        // Normalize and pre-apply log
                                        var invSum      = 1.0f / sum;
                                        var vectorCount = PITCH_BINS / _vectorSize;
                                        var invSumVec   = new Vector<float>(invSum);
                                        var epsilonVec  = new Vector<float>(float.Epsilon);

                                        for ( var v = 0; v < vectorCount; v++ )
                                        {
                                            var idx        = y * PITCH_BINS + v * _vectorSize;
                                            var values     = new Vector<float>(transitionMatrix.AsSpan(idx, _vectorSize));
                                            var normalized = values * invSumVec;
                                            var logValues  = Vector.Log(normalized + epsilonVec);

                                            for ( var j = 0; j < _vectorSize; j++ )
                                            {
                                                transitionMatrix[idx + j] = logValues[j];
                                            }
                                        }

                                        // Handle remainder
                                        for ( var x = vectorCount * _vectorSize; x < PITCH_BINS; x++ )
                                        {
                                            var idx = y * PITCH_BINS + x;
                                            transitionMatrix[idx] = transitionMatrix[idx] * invSum;
                                            transitionMatrix[idx] = MathF.Log(transitionMatrix[idx] + float.Epsilon);
                                        }
                                    });
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private float ConvertBinToFrequency(int bin)
    {
        const float CENTS_PER_BIN = 20;
        var         cents         = CENTS_PER_BIN * bin + 1997.3794084376191f;

        return 10 * MathF.Pow(2, cents / 1200);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void FillArray(Span<float> array, float value)
    {
        var vectorCount = array.Length / _vectorSize;
        var valueVec    = new Vector<float>(value);

        for ( var v = 0; v < vectorCount; v++ )
        {
            valueVec.CopyTo(array.Slice(v * _vectorSize, _vectorSize));
        }

        // Handle remainder
        for ( var i = vectorCount * _vectorSize; i < array.Length; i++ )
        {
            array[i] = value;
        }
    }
}

using System.Buffers.Binary;
using System.Runtime.CompilerServices;

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Provides functionality for converting audio between different formats.
/// </summary>
public static class AudioConverter
{
    /// <summary>
    ///     Calculates the required size for a buffer to hold the converted audio.
    /// </summary>
    /// <param name="sourceBuffer">The source audio buffer.</param>
    /// <param name="sourceFormat">The format of the source audio.</param>
    /// <param name="targetFormat">The format for the target audio.</param>
    /// <returns>The size in bytes needed for the target buffer.</returns>
    public static int CalculateTargetBufferSize(
        ReadOnlyMemory<byte> sourceBuffer,
        AudioFormat          sourceFormat,
        AudioFormat          targetFormat)
    {
        // Calculate the number of frames in the source buffer
        var framesCount = sourceBuffer.Length / sourceFormat.BytesPerFrame;

        // If resampling is needed, adjust the frame count
        if ( sourceFormat.SampleRate != targetFormat.SampleRate )
        {
            framesCount = CalculateResampledFrameCount(framesCount, sourceFormat.SampleRate, targetFormat.SampleRate);
        }

        // Calculate the expected size of the target buffer
        return framesCount * targetFormat.BytesPerFrame;
    }

    /// <summary>
    ///     Calculates the number of frames after resampling.
    /// </summary>
    /// <param name="sourceFrameCount">Number of frames in the source buffer.</param>
    /// <param name="sourceSampleRate">Sample rate of the source audio.</param>
    /// <param name="targetSampleRate">Target sample rate.</param>
    /// <returns>The number of frames after resampling.</returns>
    public static int CalculateResampledFrameCount(
        int  sourceFrameCount,
        uint sourceSampleRate,
        uint targetSampleRate)
    {
        return (int)Math.Ceiling(sourceFrameCount * ((double)targetSampleRate / sourceSampleRate));
    }

    /// <summary>
    ///     Converts audio data between different formats.
    /// </summary>
    /// <param name="sourceBuffer">The source audio buffer.</param>
    /// <param name="targetBuffer">The target audio buffer to write the converted data to.</param>
    /// <param name="sourceFormat">The format of the source audio.</param>
    /// <param name="targetFormat">The format for the target audio.</param>
    /// <returns>The number of bytes written to the target buffer.</returns>
    /// <exception cref="ArgumentException">Thrown if the target buffer is too small.</exception>
    public static int Convert(
        ReadOnlyMemory<byte> sourceBuffer,
        Memory<byte>         targetBuffer,
        AudioFormat          sourceFormat,
        AudioFormat          targetFormat)
    {
        // Calculate the number of frames in the source buffer
        var sourceFramesCount = sourceBuffer.Length / sourceFormat.BytesPerFrame;

        // Check if resampling is needed
        var needsResampling = sourceFormat.SampleRate != targetFormat.SampleRate;

        // Calculate the expected number of frames in the target buffer
        var targetFramesCount = needsResampling
                                    ? CalculateResampledFrameCount(sourceFramesCount, sourceFormat.SampleRate, targetFormat.SampleRate)
                                    : sourceFramesCount;

        // Calculate the expected size of the target buffer
        var expectedTargetSize = targetFramesCount * targetFormat.BytesPerFrame;

        if ( targetBuffer.Length < expectedTargetSize )
        {
            throw new ArgumentException("Target buffer is too small for the converted audio.");
        }

        // Fast path for same format conversion with no resampling
        if ( !needsResampling &&
             sourceFormat.Channels == targetFormat.Channels &&
             sourceFormat.BitsPerSample == targetFormat.BitsPerSample )
        {
            sourceBuffer.CopyTo(targetBuffer);

            return sourceBuffer.Length;
        }

        // If only resampling is needed (same format otherwise)
        if ( needsResampling &&
             sourceFormat.Channels == targetFormat.Channels &&
             sourceFormat.BitsPerSample == targetFormat.BitsPerSample )
        {
            return ResampleDirect(
                                  sourceBuffer,
                                  targetBuffer,
                                  sourceFormat,
                                  targetFormat,
                                  sourceFramesCount,
                                  targetFramesCount);
        }

        // For mono-to-stereo int16 conversion, use optimized path
        if ( !needsResampling &&
             sourceFormat.Channels == 1 && targetFormat.Channels == 2 &&
             sourceFormat.BitsPerSample == 32 && targetFormat.BitsPerSample == 16 )
        {
            ConvertMonoFloat32ToStereoInt16Direct(sourceBuffer, targetBuffer, sourceFramesCount);

            return expectedTargetSize;
        }

        // For stereo-to-mono conversion, use optimized path
        if ( !needsResampling &&
             sourceFormat.Channels == 2 && targetFormat.Channels == 1 &&
             sourceFormat.BitsPerSample == 16 && targetFormat.BitsPerSample == 32 )
        {
            ConvertStereoInt16ToMonoFloat32Direct(sourceBuffer, targetBuffer, sourceFramesCount);

            return expectedTargetSize;
        }

        // For mono-int16 to stereo-float32 conversion, use optimized path
        if ( !needsResampling &&
             sourceFormat.Channels == 1 && targetFormat.Channels == 2 &&
             sourceFormat.BitsPerSample == 16 && targetFormat.BitsPerSample == 32 )
        {
            ConvertMonoInt16ToStereoFloat32Direct(sourceBuffer, targetBuffer, sourceFramesCount);

            return expectedTargetSize;
        }

        // General case: convert through intermediate float format
        return ConvertGeneral(
                              sourceBuffer,
                              targetBuffer,
                              sourceFormat,
                              targetFormat,
                              sourceFramesCount,
                              targetFramesCount,
                              needsResampling);
    }

    /// <summary>
    ///     Specialized fast path to convert 48kHz stereo float32 audio to 16kHz mono int16 audio.
    ///     This optimized method combines resampling, channel conversion, and bit depth conversion in one pass.
    /// </summary>
    /// <param name="stereoFloat32Buffer">The 48kHz stereo float32 audio buffer.</param>
    /// <param name="targetBuffer">The target buffer to receive the 16kHz mono int16 data.</param>
    /// <returns>The number of bytes written to the target buffer.</returns>
    public static int ConvertStereoFloat32_48kTo_MonoInt16_16k(
        ReadOnlyMemory<byte> stereoFloat32Buffer,
        Memory<byte>         targetBuffer)
    {
        var sourceFormat = new AudioFormat(2, 32, 48000);
        var targetFormat = new AudioFormat(1, 16, 16000);

        // Calculate number of source and target frames
        var sourceFramesCount = stereoFloat32Buffer.Length / sourceFormat.BytesPerFrame;
        var targetFramesCount = CalculateResampledFrameCount(sourceFramesCount, 48000, 16000);

        // Calculate expected target size and verify buffer is large enough
        var expectedTargetSize = targetFramesCount * targetFormat.BytesPerFrame;
        if ( targetBuffer.Length < expectedTargetSize )
        {
            throw new ArgumentException("Target buffer is too small for the converted audio.");
        }

        // Process the conversion directly
        ConvertStereoFloat32_48kTo_MonoInt16_16kDirect(
                                                       stereoFloat32Buffer.Span,
                                                       targetBuffer.Span,
                                                       sourceFramesCount,
                                                       targetFramesCount);

        return expectedTargetSize;
    }

    /// <summary>
    ///     Direct conversion implementation for 48kHz stereo float32 to 16kHz mono int16.
    ///     Combines downsampling (48kHz to 16kHz - 3:1 ratio), stereo to mono mixing, and float32 to int16 conversion.
    /// </summary>
    private static void ConvertStereoFloat32_48kTo_MonoInt16_16kDirect(
        ReadOnlySpan<byte> source,
        Span<byte>         target,
        int                sourceFramesCount,
        int                targetFramesCount)
    {
        // The resampling ratio is exactly 3:1 (48000/16000)
        const int resampleRatio = 3;

        // For optimal quality, we'll use a simple low-pass filter when downsampling
        // by averaging 3 consecutive frames before picking every 3rd one

        for ( var targetFrame = 0; targetFrame < targetFramesCount; targetFrame++ )
        {
            // Calculate source frame index (center of 3-frame window)
            var sourceFrameBase = targetFrame * resampleRatio;

            // Initialize accumulator for filtered sample
            var monoSampleAccumulator = 0f;
            var sampleCount           = 0;

            // Apply a simple averaging filter over a window of frames
            for ( var offset = -1; offset <= 1; offset++ )
            {
                var sourceFrameIndex = sourceFrameBase + offset;

                // Skip samples outside buffer boundary
                if ( sourceFrameIndex < 0 || sourceFrameIndex >= sourceFramesCount )
                {
                    continue;
                }

                // Read left and right float32 samples and average them to mono
                var sourceByteIndex = sourceFrameIndex * 8; // 8 bytes per stereo float32 frame
                var leftSample      = BinaryPrimitives.ReadSingleLittleEndian(source.Slice(sourceByteIndex, 4));
                var rightSample     = BinaryPrimitives.ReadSingleLittleEndian(source.Slice(sourceByteIndex + 4, 4));

                // Average stereo to mono
                var monoSample = (leftSample + rightSample) * 0.5f;

                // Accumulate filtered sample
                monoSampleAccumulator += monoSample;
                sampleCount++;
            }

            // Average samples if we have any
            var filteredSample = sampleCount > 0 ? monoSampleAccumulator / sampleCount : 0f;

            // Convert float32 to int16 (with scaling and clamping)
            var int16Sample = ClampToInt16(filteredSample * 32767f);

            // Write to target buffer
            var targetByteIndex = targetFrame * 2; // 2 bytes per mono int16 frame
            BinaryPrimitives.WriteInt16LittleEndian(target.Slice(targetByteIndex, 2), int16Sample);
        }
    }

    /// <summary>
    ///     Resamples audio using a higher quality filter.
    ///     Uses a sinc filter for better frequency response.
    /// </summary>
    private static void ResampleWithFilter(float[] source, float[] target, uint sourceRate, uint targetRate)
    {
        // For 48kHz to 16kHz, we have a 3:1 ratio
        var ratio = (double)sourceRate / targetRate;

        // Use a simple windowed-sinc filter with 8 taps for anti-aliasing
        var filterSize = 8;

        for ( var targetIndex = 0; targetIndex < target.Length; targetIndex++ )
        {
            // Calculate the corresponding position in the source
            var sourcePos         = targetIndex * ratio;
            var sourceCenterIndex = (int)sourcePos;

            // Apply the filter
            var sum         = 0f;
            var totalWeight = 0f;

            for ( var tap = -filterSize / 2; tap < filterSize / 2; tap++ )
            {
                var sourceIndex = sourceCenterIndex + tap;

                // Skip samples outside buffer boundary
                if ( sourceIndex < 0 || sourceIndex >= source.Length )
                {
                    continue;
                }

                // Calculate the sinc weight
                var x      = sourcePos - sourceIndex;
                var weight = x == 0 ? 1.0f : (float)(Math.Sin(Math.PI * x) / (Math.PI * x));

                // Apply a Hann window to reduce ringing
                weight *= 0.5f * (1 + (float)Math.Cos(2 * Math.PI * (tap + filterSize / 2) / filterSize));

                sum         += source[sourceIndex] * weight;
                totalWeight += weight;
            }

            // Normalize the output
            target[targetIndex] = totalWeight > 0 ? sum / totalWeight : 0f;
        }
    }

    /// <summary>
    ///     Resamples audio data to a different sample rate.
    /// </summary>
    /// <param name="sourceBuffer">The source audio buffer.</param>
    /// <param name="targetBuffer">The target audio buffer to write the resampled data to.</param>
    /// <param name="sourceFormat">The format of the source audio.</param>
    /// <param name="targetSampleRate">The target sample rate.</param>
    /// <returns>The number of bytes written to the target buffer.</returns>
    public static int Resample(
        ReadOnlyMemory<byte> sourceBuffer,
        Memory<byte>         targetBuffer,
        AudioFormat          sourceFormat,
        uint                 targetSampleRate)
    {
        // Create target format with the new sample rate but same other parameters
        var targetFormat = new AudioFormat(
                                           sourceFormat.Channels,
                                           sourceFormat.BitsPerSample,
                                           targetSampleRate);

        return Convert(sourceBuffer, targetBuffer, sourceFormat, targetFormat);
    }

    /// <summary>
    ///     Resamples floating-point audio samples directly.
    /// </summary>
    /// <param name="sourceSamples">The source float samples.</param>
    /// <param name="targetSamples">The target buffer to write the resampled samples to.</param>
    /// <param name="channels">Number of channels in the audio.</param>
    /// <param name="sourceSampleRate">Source sample rate.</param>
    /// <param name="targetSampleRate">Target sample rate.</param>
    /// <returns>The number of frames written to the target buffer.</returns>
    public static int ResampleFloat(
        ReadOnlyMemory<float> sourceSamples,
        Memory<float>         targetSamples,
        ushort                channels,
        uint                  sourceSampleRate,
        uint                  targetSampleRate)
    {
        // Fast path for same sample rate
        if ( sourceSampleRate == targetSampleRate )
        {
            sourceSamples.CopyTo(targetSamples);

            return sourceSamples.Length / channels;
        }

        var sourceFramesCount = sourceSamples.Length / channels;
        var targetFramesCount = CalculateResampledFrameCount(sourceFramesCount, sourceSampleRate, targetSampleRate);

        // Ensure target buffer is large enough
        if ( targetSamples.Length < targetFramesCount * channels )
        {
            throw new ArgumentException("Target buffer is too small for the resampled audio.");
        }

        // Perform the resampling
        ResampleFloatBuffer(
                            sourceSamples.Span,
                            targetSamples.Span,
                            channels,
                            sourceFramesCount,
                            targetFramesCount);

        return targetFramesCount;
    }

    /// <summary>
    ///     Converts float samples to a different format (channels, sample rate) and outputs as byte array.
    /// </summary>
    /// <param name="sourceSamples">The source float samples.</param>
    /// <param name="targetBuffer">The target buffer to write the converted data to.</param>
    /// <param name="sourceChannels">Number of channels in the source.</param>
    /// <param name="sourceSampleRate">Source sample rate.</param>
    /// <param name="targetFormat">The desired output format.</param>
    /// <returns>The number of bytes written to the target buffer.</returns>
    public static int ConvertFloat(
        ReadOnlyMemory<float> sourceSamples,
        Memory<byte>          targetBuffer,
        ushort                sourceChannels,
        uint                  sourceSampleRate,
        AudioFormat           targetFormat)
    {
        var sourceFramesCount = sourceSamples.Length / sourceChannels;
        var needsResampling   = sourceSampleRate != targetFormat.SampleRate;

        // Calculate target frames count after potential resampling
        var targetFramesCount = needsResampling
                                    ? CalculateResampledFrameCount(sourceFramesCount, sourceSampleRate, targetFormat.SampleRate)
                                    : sourceFramesCount;

        // Calculate expected target buffer size
        var expectedTargetSize = targetFramesCount * targetFormat.BytesPerFrame;

        if ( targetBuffer.Length < expectedTargetSize )
        {
            throw new ArgumentException("Target buffer is too small for the converted audio.");
        }

        // Handle resampling if needed
        ReadOnlyMemory<float> resampledSamples;
        if ( needsResampling )
        {
            var resampledBuffer = new float[targetFramesCount * sourceChannels];
            ResampleFloatBuffer(
                                sourceSamples.Span,
                                resampledBuffer.AsSpan(),
                                sourceChannels,
                                sourceFramesCount,
                                targetFramesCount);

            resampledSamples = resampledBuffer;
        }
        else
        {
            resampledSamples = sourceSamples;
        }

        // Handle channel conversion if needed
        ReadOnlyMemory<float> convertedSamples;
        if ( sourceChannels != targetFormat.Channels )
        {
            var convertedBuffer = new float[targetFramesCount * targetFormat.Channels];
            ConvertChannels(
                            resampledSamples.Span,
                            convertedBuffer.AsSpan(),
                            sourceChannels,
                            targetFormat.Channels,
                            targetFramesCount);

            convertedSamples = convertedBuffer;
        }
        else
        {
            convertedSamples = resampledSamples;
        }

        // Serialize to the target format
        SampleSerializer.Serialize(convertedSamples, targetBuffer, targetFormat.BitsPerSample);

        return expectedTargetSize;
    }

    /// <summary>
    ///     Converts audio format with direct access to float samples.
    /// </summary>
    /// <param name="sourceSamples">The source float samples.</param>
    /// <param name="targetSamples">The target buffer to write the converted samples to.</param>
    /// <param name="sourceChannels">Number of channels in the source.</param>
    /// <param name="targetChannels">Number of channels for the output.</param>
    /// <param name="sourceSampleRate">Source sample rate.</param>
    /// <param name="targetSampleRate">Target sample rate.</param>
    /// <returns>The number of frames written to the target buffer.</returns>
    public static int ConvertFloat(
        ReadOnlyMemory<float> sourceSamples,
        Memory<float>         targetSamples,
        ushort                sourceChannels,
        ushort                targetChannels,
        uint                  sourceSampleRate,
        uint                  targetSampleRate)
    {
        var sourceFramesCount      = sourceSamples.Length / sourceChannels;
        var needsResampling        = sourceSampleRate != targetSampleRate;
        var needsChannelConversion = sourceChannels != targetChannels;

        // If no conversion needed, just copy
        if ( !needsResampling && !needsChannelConversion )
        {
            sourceSamples.CopyTo(targetSamples);

            return sourceFramesCount;
        }

        // Calculate target frames count after potential resampling
        var targetFramesCount = needsResampling
                                    ? CalculateResampledFrameCount(sourceFramesCount, sourceSampleRate, targetSampleRate)
                                    : sourceFramesCount;

        // Ensure target buffer is large enough
        if ( targetSamples.Length < targetFramesCount * targetChannels )
        {
            throw new ArgumentException("Target buffer is too small for the converted audio.");
        }

        // Optimize the common path where only channel conversion or only resampling is needed
        if ( needsResampling && !needsChannelConversion )
        {
            // Only resample
            ResampleFloatBuffer(
                                sourceSamples.Span,
                                targetSamples.Span,
                                sourceChannels, // same as targetChannels in this case
                                sourceFramesCount,
                                targetFramesCount);

            return targetFramesCount;
        }

        if ( !needsResampling && needsChannelConversion )
        {
            // Only convert channels
            ConvertChannels(
                            sourceSamples.Span,
                            targetSamples.Span,
                            sourceChannels,
                            targetChannels,
                            sourceFramesCount);

            return sourceFramesCount;
        }

        // If we need both resampling and channel conversion
        // First resample, then convert channels
        var resampledBuffer = needsResampling ? new float[targetFramesCount * sourceChannels] : null;

        if ( needsResampling )
        {
            ResampleFloatBuffer(
                                sourceSamples.Span,
                                resampledBuffer.AsSpan(),
                                sourceChannels,
                                sourceFramesCount,
                                targetFramesCount);
        }

        // Then convert channels
        ConvertChannels(
                        needsResampling ? resampledBuffer.AsSpan() : sourceSamples.Span,
                        targetSamples.Span,
                        sourceChannels,
                        targetChannels,
                        targetFramesCount);

        return targetFramesCount;
    }

    /// <summary>
    ///     Direct resampling of audio data without format conversion.
    /// </summary>
    private static int ResampleDirect(
        ReadOnlyMemory<byte> sourceBuffer,
        Memory<byte>         targetBuffer,
        AudioFormat          sourceFormat,
        AudioFormat          targetFormat,
        int                  sourceFramesCount,
        int                  targetFramesCount)
    {
        // Convert to float for processing
        var floatSamples = new float[sourceFramesCount * sourceFormat.Channels];
        SampleSerializer.Deserialize(sourceBuffer, floatSamples.AsMemory(), sourceFormat.BitsPerSample);

        // Resample the float samples
        var resampledBuffer = new float[targetFramesCount * targetFormat.Channels];
        ResampleFloatBuffer(
                            floatSamples.AsSpan(),
                            resampledBuffer.AsSpan(),
                            sourceFormat.Channels,
                            sourceFramesCount,
                            targetFramesCount);

        // Serialize back to the target format
        SampleSerializer.Serialize(resampledBuffer, targetBuffer, targetFormat.BitsPerSample);

        return targetFramesCount * targetFormat.BytesPerFrame;
    }

    /// <summary>
    ///     Resamples floating-point audio samples.
    /// </summary>
    private static void ResampleFloatBuffer(
        ReadOnlySpan<float> sourceBuffer,
        Span<float>         targetBuffer,
        ushort              channels,
        int                 sourceFramesCount,
        int                 targetFramesCount)
    {
        // Calculate the step size for linear interpolation
        var step = (double)(sourceFramesCount - 1) / (targetFramesCount - 1);

        for ( var targetFrame = 0; targetFrame < targetFramesCount; targetFrame++ )
        {
            // Calculate source position (as a floating point value)
            var sourcePos = targetFrame * step;

            // Get indices of the two source frames to interpolate between
            var sourceFrameLow  = (int)sourcePos;
            var sourceFrameHigh = Math.Min(sourceFrameLow + 1, sourceFramesCount - 1);

            // Calculate interpolation factor
            var fraction = (float)(sourcePos - sourceFrameLow);

            // Interpolate each channel
            for ( var channel = 0; channel < channels; channel++ )
            {
                var sourceLowIndex  = sourceFrameLow * channels + channel;
                var sourceHighIndex = sourceFrameHigh * channels + channel;
                var targetIndex     = targetFrame * channels + channel;

                // Linear interpolation
                targetBuffer[targetIndex] = Lerp(
                                                 sourceBuffer[sourceLowIndex],
                                                 sourceBuffer[sourceHighIndex],
                                                 fraction);
            }
        }
    }

    /// <summary>
    ///     Linear interpolation between two values.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static float Lerp(float a, float b, float t) { return a + (b - a) * t; }

    /// <summary>
    ///     Converts mono float32 audio to stereo int16 PCM format.
    /// </summary>
    /// <param name="monoFloat32Buffer">The mono float32 audio buffer.</param>
    /// <param name="targetBuffer">The target buffer to receive the stereo int16 PCM data.</param>
    /// <param name="sampleRate">The sample rate of the audio (preserved in the conversion).</param>
    /// <returns>The number of bytes written to the target buffer.</returns>
    public static int ConvertMonoFloat32ToStereoInt16(
        ReadOnlyMemory<byte> monoFloat32Buffer,
        Memory<byte>         targetBuffer,
        uint                 sampleRate = 44100)
    {
        var sourceFormat = AudioFormat.CreateMono(32, sampleRate);
        var targetFormat = AudioFormat.CreateStereo(16, sampleRate);

        return Convert(monoFloat32Buffer, targetBuffer, sourceFormat, targetFormat);
    }

    /// <summary>
    ///     Converts mono int16 audio to stereo float32 PCM format.
    /// </summary>
    /// <param name="monoInt16Buffer">The mono int16 audio buffer.</param>
    /// <param name="targetBuffer">The target buffer to receive the stereo float32 PCM data.</param>
    /// <param name="sampleRate">The sample rate of the audio (preserved in the conversion).</param>
    /// <returns>The number of bytes written to the target buffer.</returns>
    public static int ConvertMonoInt16ToStereoFloat32(
        ReadOnlyMemory<byte> monoInt16Buffer,
        Memory<byte>         targetBuffer,
        uint                 sampleRate = 44100)
    {
        var sourceFormat = AudioFormat.CreateMono(16, sampleRate);
        var targetFormat = AudioFormat.CreateStereo(32, sampleRate);

        // Use fast path for this specific conversion
        var framesCount        = monoInt16Buffer.Length / sourceFormat.BytesPerFrame;
        var expectedTargetSize = framesCount * targetFormat.BytesPerFrame;

        if ( targetBuffer.Length < expectedTargetSize )
        {
            throw new ArgumentException("Target buffer is too small for the converted audio.");
        }

        ConvertMonoInt16ToStereoFloat32Direct(monoInt16Buffer, targetBuffer, framesCount);

        return expectedTargetSize;
    }

    /// <summary>
    ///     Optimized direct conversion from mono float32 to stereo int16.
    /// </summary>
    private static void ConvertMonoFloat32ToStereoInt16Direct(
        ReadOnlyMemory<byte> source,
        Memory<byte>         target,
        int                  framesCount)
    {
        var sourceSpan = source.Span;
        var targetSpan = target.Span;

        for ( var frame = 0; frame < framesCount; frame++ )
        {
            var sourceIndex = frame * 4; // 4 bytes per float32
            var targetIndex = frame * 4; // 2 bytes per int16 * 2 channels

            // Read float32 value
            var floatValue = BinaryPrimitives.ReadSingleLittleEndian(sourceSpan.Slice(sourceIndex, 4));

            // Convert to int16 (with clamping)
            var int16Value = ClampToInt16(floatValue * 32767f);

            // Write the same value to both left and right channels
            BinaryPrimitives.WriteInt16LittleEndian(targetSpan.Slice(targetIndex, 2), int16Value);
            BinaryPrimitives.WriteInt16LittleEndian(targetSpan.Slice(targetIndex + 2, 2), int16Value);
        }
    }

    /// <summary>
    ///     Optimized direct conversion from stereo int16 to mono float32.
    /// </summary>
    private static void ConvertStereoInt16ToMonoFloat32Direct(
        ReadOnlyMemory<byte> source,
        Memory<byte>         target,
        int                  framesCount)
    {
        var sourceSpan = source.Span;
        var targetSpan = target.Span;

        for ( var frame = 0; frame < framesCount; frame++ )
        {
            var sourceIndex = frame * 4; // 2 bytes per int16 * 2 channels
            var targetIndex = frame * 4; // 4 bytes per float32

            // Read int16 values for left and right channels
            var leftValue  = BinaryPrimitives.ReadInt16LittleEndian(sourceSpan.Slice(sourceIndex, 2));
            var rightValue = BinaryPrimitives.ReadInt16LittleEndian(sourceSpan.Slice(sourceIndex + 2, 2));

            // Convert to float32 and average the channels
            var floatValue = (leftValue + rightValue) * 0.5f / 32768f;

            // Write to target buffer
            BinaryPrimitives.WriteSingleLittleEndian(targetSpan.Slice(targetIndex, 4), floatValue);
        }
    }

    /// <summary>
    ///     Optimized direct conversion from mono int16 to stereo float32.
    /// </summary>
    private static void ConvertMonoInt16ToStereoFloat32Direct(
        ReadOnlyMemory<byte> source,
        Memory<byte>         target,
        int                  framesCount)
    {
        var sourceSpan = source.Span;
        var targetSpan = target.Span;

        for ( var frame = 0; frame < framesCount; frame++ )
        {
            var sourceIndex = frame * 2; // 2 bytes per int16
            var targetIndex = frame * 8; // 4 bytes per float32 * 2 channels

            // Read int16 value
            var int16Value = BinaryPrimitives.ReadInt16LittleEndian(sourceSpan.Slice(sourceIndex, 2));

            // Convert to float32
            var floatValue = int16Value / 32768f;

            // Write the same float value to both left and right channels
            BinaryPrimitives.WriteSingleLittleEndian(targetSpan.Slice(targetIndex, 4), floatValue);
            BinaryPrimitives.WriteSingleLittleEndian(targetSpan.Slice(targetIndex + 4, 4), floatValue);
        }
    }

    /// <summary>
    ///     General case conversion using intermediate float format.
    /// </summary>
    private static int ConvertGeneral(
        ReadOnlyMemory<byte> sourceBuffer,
        Memory<byte>         targetBuffer,
        AudioFormat          sourceFormat,
        AudioFormat          targetFormat,
        int                  sourceFramesCount,
        int                  targetFramesCount,
        bool                 needsResampling)
    {
        // Deserialize to float samples - this will give us interleaved float samples
        var floatSamples = new float[sourceFramesCount * sourceFormat.Channels];
        SampleSerializer.Deserialize(sourceBuffer, floatSamples.AsMemory(), sourceFormat.BitsPerSample);

        // Perform resampling if needed
        Memory<float> resampledSamples;
        int           actualFrameCount;

        if ( needsResampling )
        {
            var resampledBuffer = new float[targetFramesCount * sourceFormat.Channels];
            ResampleFloatBuffer(
                                floatSamples.AsSpan(),
                                resampledBuffer.AsSpan(),
                                sourceFormat.Channels,
                                sourceFramesCount,
                                targetFramesCount);

            resampledSamples = resampledBuffer;
            actualFrameCount = targetFramesCount;
        }
        else
        {
            resampledSamples = floatSamples;
            actualFrameCount = sourceFramesCount;
        }

        // Convert channel configuration if needed
        Memory<float> convertedSamples;

        if ( sourceFormat.Channels != targetFormat.Channels )
        {
            var convertedBuffer = new float[actualFrameCount * targetFormat.Channels];
            ConvertChannels(
                            resampledSamples.Span,
                            convertedBuffer.AsSpan(),
                            sourceFormat.Channels,
                            targetFormat.Channels,
                            actualFrameCount);

            convertedSamples = convertedBuffer;
        }
        else
        {
            // No channel conversion needed
            convertedSamples = resampledSamples;
        }

        // Serialize to the target format
        SampleSerializer.Serialize(convertedSamples, targetBuffer, targetFormat.BitsPerSample);

        return actualFrameCount * targetFormat.BytesPerFrame;
    }

    /// <summary>
    ///     Converts between different channel configurations.
    /// </summary>
    private static void ConvertChannels(
        ReadOnlySpan<float> source,
        Span<float>         target,
        ushort              sourceChannels,
        ushort              targetChannels,
        int                 framesCount)
    {
        // If source and target have the same number of channels, just copy
        if ( sourceChannels == targetChannels )
        {
            source.CopyTo(target);

            return;
        }

        // Handle specific conversions with optimized implementations
        if ( sourceChannels == 1 && targetChannels == 2 )
        {
            // Mono to stereo conversion
            ConvertMonoToStereo(source, target, framesCount);
        }
        else if ( sourceChannels == 2 && targetChannels == 1 )
        {
            // Stereo to mono conversion
            ConvertStereoToMono(source, target, framesCount);
        }
        else
        {
            // More general conversion implementation
            ConvertChannelsGeneral(source, target, sourceChannels, targetChannels, framesCount);
        }
    }

    /// <summary>
    ///     Converts mono audio to stereo by duplicating each sample.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void ConvertMonoToStereo(
        ReadOnlySpan<float> source,
        Span<float>         target,
        int                 framesCount)
    {
        for ( var frame = 0; frame < framesCount; frame++ )
        {
            var sourceSample = source[frame];
            var targetIndex  = frame * 2;

            target[targetIndex]     = sourceSample; // Left channel
            target[targetIndex + 1] = sourceSample; // Right channel
        }
    }

    /// <summary>
    ///     Converts stereo audio to mono by averaging the channels.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void ConvertStereoToMono(
        ReadOnlySpan<float> source,
        Span<float>         target,
        int                 framesCount)
    {
        for ( var frame = 0; frame < framesCount; frame++ )
        {
            var sourceIndex = frame * 2;
            var leftSample  = source[sourceIndex];
            var rightSample = source[sourceIndex + 1];

            target[frame] = (leftSample + rightSample) * 0.5f; // Average the channels
        }
    }

    /// <summary>
    ///     General method for converting between different channel configurations.
    /// </summary>
    private static void ConvertChannelsGeneral(
        ReadOnlySpan<float> source,
        Span<float>         target,
        ushort              sourceChannels,
        ushort              targetChannels,
        int                 framesCount)
    {
        ConvertChannelsChunk(source, target, sourceChannels, targetChannels, 0, framesCount);
    }

    /// <summary>
    ///     Converts a chunk of frames between different channel configurations.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void ConvertChannelsChunk(
        ReadOnlySpan<float> source,
        Span<float>         target,
        ushort              sourceChannels,
        ushort              targetChannels,
        int                 startFrame,
        int                 endFrame)
    {
        for ( var frame = startFrame; frame < endFrame; frame++ )
        {
            var sourceFrameOffset = frame * sourceChannels;
            var targetFrameOffset = frame * targetChannels;

            // Find the minimum of source and target channels
            var minChannels = Math.Min(sourceChannels, targetChannels);

            // Copy the available channels
            for ( var channel = 0; channel < minChannels; channel++ )
            {
                target[targetFrameOffset + channel] = source[sourceFrameOffset + channel];
            }

            // If target has more channels than source, duplicate the last channel
            for ( var channel = minChannels; channel < targetChannels; channel++ )
            {
                target[targetFrameOffset + channel] = source[sourceFrameOffset + (minChannels - 1)];
            }
        }
    }

    /// <summary>
    ///     Clamps a float value to the range of a 16-bit integer.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static short ClampToInt16(float value)
    {
        if ( value > 32767f )
        {
            return 32767;
        }

        if ( value < -32768f )
        {
            return -32768;
        }

        return (short)value;
    }
}

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

using NAudio.Wave;

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents a source that captures audio from a microphone,
///     configured dynamically via IOptionsMonitor.
/// </summary>
public sealed class MicrophoneInputNAudioSource : AwaitableWaveFileSource, IMicrophone
{
    private readonly Lock _lock = new();

    private readonly ILogger _logger;

    private readonly IDisposable? _optionsChangeListener;

    private MicrophoneConfiguration _currentOptions;

    private bool _isDisposed = false;

    private CancellationTokenSource? _recordingCts;

    private bool _wasRecordingBeforeReconfigure = false;

    private WaveInEvent? _waveIn;

    public MicrophoneInputNAudioSource(
        ILogger<MicrophoneInputNAudioSource>     logger,
        IOptionsMonitor<MicrophoneConfiguration> optionsMonitor,
        IChannelAggregationStrategy?             aggregationStrategy = null)
        : base(new Dictionary<string, string>(),
               true,
               false,
               DefaultInitialSize,
               DefaultInitialSize,
               aggregationStrategy)
    {
        _logger         = logger ?? throw new ArgumentNullException(nameof(logger));
        _currentOptions = optionsMonitor.CurrentValue;

        _logger.LogInformation("Initializing MicrophoneInputNAudioSource with options: {@Options}", _currentOptions);

        InitializeMicrophone(_currentOptions);

        _optionsChangeListener = optionsMonitor.OnChange(HandleOptionsChange);
    }

    public void StartRecording()
    {
        lock (_lock)
        {
            ObjectDisposedException.ThrowIf(_isDisposed, typeof(MicrophoneInputNAudioSource));

            if ( _recordingCts is { IsCancellationRequested: false } )
            {
                _logger.LogWarning("StartRecording called, but recording is already active.");

                return;
            }

            StartRecordingInternal();
        }
    }

    public void StopRecording()
    {
        lock (_lock)
        {
            if ( _isDisposed )
            {
                return;
            }

            if ( _recordingCts == null || _recordingCts.IsCancellationRequested )
            {
                return;
            }

            StopInternal();
        }
    }

    public IEnumerable<string> GetAvailableDevices() { return GetAvailableDevicesInternal(_logger); }

    public new void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private static IEnumerable<string> GetAvailableDevicesInternal(ILogger? logger = null)
    {
        var deviceNames = new List<string>();
        try
        {
            var deviceCount = WaveInEvent.DeviceCount;
            logger?.LogDebug("Enumerating {DeviceCount} audio input devices.", deviceCount);
            for ( var n = 0; n < deviceCount; n++ )
            {
                try
                {
                    var caps = WaveInEvent.GetCapabilities(n);
                    deviceNames.Add(caps.ProductName ?? $"Unnamed Device {n}");
                    logger?.LogTrace("Found Device {DeviceNumber}: {ProductName}", n, caps.ProductName);
                }
                catch (Exception capEx)
                {
                    logger?.LogError(capEx, "Error getting capabilities for WaveInEvent device number {DeviceNumber}.", n);
                    deviceNames.Add($"⚠ {n}");
                }
            }
        }
        catch (Exception ex)
        {
            logger?.LogError(ex, "Error enumerating WaveInEvent devices.");

            return [];
        }

        return deviceNames;
    }

    private void HandleOptionsChange(MicrophoneConfiguration newOptions)
    {
        lock (_lock)
        {
            if ( _isDisposed )
            {
                return;
            }

            if ( newOptions.DeviceName == _currentOptions.DeviceName )
            {
                return;
            }

            _logger.LogInformation("Audio input configuration changed. Reconfiguring microphone. New options: {@Options}", newOptions);
            _currentOptions = newOptions;

            _wasRecordingBeforeReconfigure = _recordingCts is { IsCancellationRequested: false };

            StopInternal();

            InitializeMicrophone(newOptions);

            if ( !_wasRecordingBeforeReconfigure )
            {
                return;
            }

            _logger.LogInformation("Restarting recording after reconfiguration.");
            StartRecordingInternal();
        }
    }

    private void InitializeMicrophone(MicrophoneConfiguration options)
    {
        lock (_lock)
        {
            if ( _isDisposed )
            {
                return;
            }

            DisposeMicrophoneInstance();

            var deviceNumber = FindDeviceNumberByName(options.DeviceName);
            if ( deviceNumber == -1 )
            {
                _logger.LogWarning("Could not find audio input device named '{DeviceName}'. Falling back to default device (0).", options.DeviceName);
                deviceNumber = 0;
            }

            try
            {
                _waveIn = new WaveInEvent { DeviceNumber = deviceNumber, WaveFormat = new WaveFormat(16000, 16, 1), BufferMilliseconds = 100 };

                if ( !IsInitialized )
                {
                    Initialize(new AudioSourceHeader { BitsPerSample = 16, Channels = 1, SampleRate = 16000 });
                }

                _waveIn.DataAvailable    += WaveIn_DataAvailable;
                _waveIn.RecordingStopped += WaveInRecordingStopped;

                _logger.LogInformation("Microphone initialized with DeviceNumber: {DeviceNumber}, WaveFormat: {WaveFormat}",
                                       _waveIn.DeviceNumber, _waveIn.WaveFormat);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to initialize WaveInEvent with DeviceNumber {DeviceNumber} and options {@Options}", deviceNumber, options);
                _waveIn = null;
            }
        }
    }

    private int FindDeviceNumberByName(string? deviceName)
    {
        if ( string.IsNullOrWhiteSpace(deviceName) )
        {
            _logger.LogDebug("Device name is null or whitespace, returning default device number 0.");

            return 0;
        }

        try
        {
            var devices = GetAvailableDevicesInternal();
            var index   = 0;
            foreach ( var name in devices )
            {
                _logger.LogTrace("Checking device {DeviceNumber}: {ProductName}", index, name);
                if ( deviceName.Trim().Equals(name?.Trim(), StringComparison.OrdinalIgnoreCase) )
                {
                    _logger.LogDebug("Found device '{DeviceName}' at DeviceNumber {DeviceNumber}", deviceName, index);

                    return index;
                }

                index++;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error searching for device name '{DeviceName}'.", deviceName);
        }

        _logger.LogWarning("Audio input device named '{DeviceName}' not found among available devices.", deviceName);

        return -1;
    }

    private void StartRecordingInternal()
    {
        if ( _waveIn == null )
        {
            _logger.LogError("Cannot start recording, microphone is not initialized (likely due to previous error).");

            return;
        }

        try
        {
            _recordingCts = new CancellationTokenSource();
            _waveIn.StartRecording();
            _logger.LogInformation("Microphone recording started.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to start microphone recording.");
            _recordingCts?.Dispose();
            _recordingCts = null;

            throw;
        }
    }

    private void StopInternal()
    {
        if ( _waveIn == null || _recordingCts == null || _recordingCts.IsCancellationRequested )
        {
            return;
        }

        _logger.LogInformation("Stopping microphone recording...");
        try
        {
            _recordingCts.Cancel();
            _waveIn?.StopRecording();

            _logger.LogInformation("Microphone recording stopped.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during microphone StopRecording.");
        }
        finally
        {
            _recordingCts?.Dispose();
            _recordingCts = null;
        }
    }

    private void WaveIn_DataAvailable(object? sender, WaveInEventArgs e)
    {
        if ( _recordingCts is not { IsCancellationRequested: false } )
        {
            return;
        }

        var sourceSpan = e.Buffer.AsMemory(0, e.BytesRecorded);
        WriteData(sourceSpan);
    }

    private void WaveInRecordingStopped(object? sender, StoppedEventArgs e)
    {
        _logger.LogDebug("MicrophoneIn_RecordingStopped event received.");
        if ( e.Exception != null )
        {
            _logger.LogError(e.Exception, "An error occurred during microphone recording.");
            // Decide how to handle this - maybe set an error state?
            // For now, just log it. Reconfiguration might fix it later.
        }

        _logger.LogDebug("Microphone stream flushed after stopping.");
    }

    private void DisposeMicrophoneInstance()
    {
        if ( _waveIn == null )
        {
            return;
        }

        _logger.LogDebug("Disposing existing WaveInEvent instance.");
        _waveIn.DataAvailable    -= WaveIn_DataAvailable;
        _waveIn.RecordingStopped -= WaveInRecordingStopped;

        if ( _recordingCts is { IsCancellationRequested: false } )
        {
            _logger.LogWarning("Disposing microphone instance while it was potentially still marked as recording. Stopping first.");
            StopInternal();
        }

        _waveIn.Dispose();
        _waveIn = null;
    }

    protected override void Dispose(bool disposing)
    {
        if ( _isDisposed )
        {
            return;
        }

        if ( disposing )
        {
            _logger.LogInformation("Disposing MicrophoneInputNAudioSource...");
            lock (_lock)
            {
                _isDisposed = true;
                _optionsChangeListener?.Dispose();

                StopInternal();
                DisposeMicrophoneInstance();

                Flush();

                _recordingCts?.Dispose();
                _recordingCts = null;
            }

            _logger.LogInformation("MicrophoneInputNAudioSource disposed.");
        }

        base.Dispose(disposing);
    }
}

using System.Runtime.InteropServices;
using System.Threading.Channels;

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Audio.Player;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.TTS.Synthesis;

using PortAudioSharp;

using Stream = PortAudioSharp.Stream;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Adapters.Audio.Output;

public class PortaudioOutputAdapter(ILogger<PortaudioOutputAdapter> logger, IAudioProgressNotifier progressNotifier) : IAudioOutputAdapter
{
    private const int DefaultSampleRate = 24000;

    private const int DefaultFrameBufferSize = 1024;

    private readonly float[] _frameBuffer = new float[DefaultFrameBufferSize];

    private Stream? _audioStream;

    private AudioBuffer? _currentBuffer;

    private ChannelReader<TtsChunkEvent>? _currentReader;

    private Guid _currentTurnId;

    private long _framesOfChunkPlayed;

    private TaskCompletionSource<bool>? _playbackCompletion;

    private CancellationTokenSource? _playbackCts;

    private volatile bool _producerCompleted;

    private Channel<IAudioProgressEvent>? _progressChannel;

    private Guid _sessionId;

    public ValueTask DisposeAsync()
    {
        try
        {
            _audioStream = null;
        }
        catch
        {
            /* ignored */
        }

        return ValueTask.CompletedTask;
    }

    public Guid AdapterId { get; } = Guid.NewGuid();

    public ValueTask InitializeAsync(Guid sessionId, CancellationToken cancellationToken)
    {
        _sessionId = sessionId;

        try
        {
            PortAudio.Initialize();
            var deviceIndex = PortAudio.DefaultOutputDevice;
            if ( deviceIndex == PortAudio.NoDevice )
            {
                throw new AudioDeviceNotFoundException("No default PortAudio output device found.");
            }

            var deviceInfo = PortAudio.GetDeviceInfo(deviceIndex);
            logger.LogDebug("Using PortAudio output device: {DeviceName} (Index: {DeviceIndex})", deviceInfo.name, deviceIndex);

            var parameters = new StreamParameters {
                                                      device                    = deviceIndex,
                                                      channelCount              = 1,
                                                      sampleFormat              = SampleFormat.Float32,
                                                      suggestedLatency          = deviceInfo.defaultLowOutputLatency,
                                                      hostApiSpecificStreamInfo = IntPtr.Zero
                                                  };

            _audioStream = new Stream(
                                      null,
                                      parameters,
                                      DefaultSampleRate,
                                      DefaultFrameBufferSize,
                                      StreamFlags.ClipOff,
                                      AudioCallback,
                                      IntPtr.Zero);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to initialize PortAudio or create stream.");

            try
            {
                PortAudio.Terminate();
            }
            catch
            {
                // ignored
            }

            throw new AudioPlayerInitializationException("Failed to initialize PortAudio audio system.", ex);
        }

        return ValueTask.CompletedTask;
    }

    public ValueTask StartAsync(CancellationToken cancellationToken)
    {
        // Don't start it here, SendAsync will handle it.

        return ValueTask.CompletedTask;
    }

    public ValueTask StopAsync(CancellationToken cancellationToken)
    {
        // Don't stop it here, SendAsync's cleanup or cancellation will handle it.
        // If a hard stop independent of SendAsync is needed, cancellation of SendAsync's token is the way.

        return ValueTask.CompletedTask;
    }

    public async Task SendAsync(ChannelReader<TtsChunkEvent> inputReader, ChannelWriter<IOutputEvent> outputWriter, Guid turnId, CancellationToken cancellationToken = default)
    {
        if ( _audioStream == null )
        {
            throw new InvalidOperationException("Audio stream is not initialized.");
        }

        _currentTurnId = turnId;
        _currentReader = inputReader;

        _progressChannel = Channel.CreateBounded<IAudioProgressEvent>(new BoundedChannelOptions(10) { SingleReader = true, SingleWriter = true });

        _playbackCts        = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _playbackCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

        var       completedReason  = CompletionReason.Completed;
        var       firstChunk       = true;
        var       localPlaybackCts = _playbackCts;
        var       localCompletion  = _playbackCompletion;
        using var eventLoopCts     = new CancellationTokenSource();
        var       progressPumpTask = ProgressEventsLoop(eventLoopCts.Token);

        try
        {
            _audioStream.Start();

            while ( await inputReader.WaitToReadAsync(localPlaybackCts.Token).ConfigureAwait(false) )
            {
                if ( !firstChunk )
                {
                    continue;
                }

                var firstChunkEvent = new AudioPlaybackStartedEvent(_sessionId, turnId, DateTimeOffset.UtcNow);
                await outputWriter.WriteAsync(firstChunkEvent, localPlaybackCts.Token).ConfigureAwait(false);

                firstChunk = false;
            }

            _producerCompleted = true;

            await localCompletion.Task.ConfigureAwait(false);
            await progressPumpTask.ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            completedReason = CompletionReason.Cancelled;

            eventLoopCts.CancelAfter(250);

            await localCompletion.Task.ConfigureAwait(false);
            await progressPumpTask.ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            completedReason = CompletionReason.Error;

            await outputWriter.WriteAsync(new ErrorOutputEvent(_sessionId, turnId, DateTimeOffset.UtcNow, ex), CancellationToken.None).ConfigureAwait(false);
        }
        finally
        {
            CleanupPlayback();

            if ( !firstChunk )
            {
                await outputWriter.WriteAsync(new AudioPlaybackEndedEvent(_sessionId, turnId, DateTimeOffset.UtcNow, completedReason), CancellationToken.None).ConfigureAwait(false);
            }
        }
    }

    private async ValueTask ProgressEventsLoop(CancellationToken ct = default)
    {
        var eventsReader = _progressChannel!.Reader;

        await foreach ( var progressEvent in eventsReader.ReadAllAsync(ct).ConfigureAwait(false) )
        {
            if ( ct.IsCancellationRequested )
            {
                break;
            }

            switch ( progressEvent )
            {
                case AudioChunkPlaybackStartedEvent startedArgs:
                    progressNotifier.RaiseChunkStarted(this, startedArgs);

                    break;
                case AudioChunkPlaybackEndedEvent endedArgs:
                    progressNotifier.RaiseChunkEnded(this, endedArgs);

                    break;
                case AudioPlaybackProgressEvent progressArgs:
                    progressNotifier.RaiseProgress(this, progressArgs);

                    break;
            }
        }
    }

    private void CleanupPlayback()
    {
        _audioStream?.Stop();

        _currentTurnId = Guid.Empty;
        _currentReader = null;

        _progressChannel?.Writer.TryComplete();
        _producerCompleted   = false;
        _framesOfChunkPlayed = 0;
        _currentBuffer       = null;

        _playbackCts?.Cancel();
        _playbackCts?.Dispose();
        _playbackCts = null;

        Array.Clear(_frameBuffer, 0, DefaultFrameBufferSize);
    }

    private StreamCallbackResult AudioCallback(IntPtr input, IntPtr output, uint framecount, ref StreamCallbackTimeInfo timeinfo, StreamCallbackFlags statusflags, IntPtr userdataptr)
    {
        var framesRequested = (int)framecount;
        var framesWritten   = 0;

        var turnId             = _currentTurnId;
        var reader             = _currentReader;
        var completionSource   = _playbackCompletion;
        var currentPlaybackCts = _playbackCts;
        var ct                 = currentPlaybackCts?.Token ?? CancellationToken.None;
        var progressWriter     = _progressChannel!.Writer;

        if ( reader == null )
        {
            completionSource?.TrySetResult(false);
            progressWriter.TryComplete();

            return StreamCallbackResult.Complete;
        }

        while ( framesWritten < framesRequested )
        {
            if ( ct.IsCancellationRequested )
            {
                FillSilence();
                completionSource?.TrySetResult(false);

                if ( _currentBuffer != null )
                {
                    progressWriter.TryWrite(new AudioChunkPlaybackEndedEvent(_sessionId, turnId, DateTimeOffset.UtcNow, _currentBuffer.Segment));
                }

                progressWriter.TryComplete();

                return StreamCallbackResult.Complete;
            }

            if ( _currentBuffer == null )
            {
                if ( reader.TryRead(out var chunk) )
                {
                    _currentBuffer = new AudioBuffer(chunk.Chunk.AudioData, chunk.Chunk);
                    progressWriter.TryWrite(new AudioChunkPlaybackStartedEvent(_sessionId, turnId, DateTimeOffset.UtcNow, chunk.Chunk));
                }
                else
                {
                    if ( _producerCompleted )
                    {
                        FillSilence();
                        completionSource?.TrySetResult(true);
                        progressWriter.TryComplete();

                        return StreamCallbackResult.Complete;
                    }

                    FillSilence();
                }
            }

            if ( _currentBuffer == null )
            {
                return StreamCallbackResult.Continue;
            }

            var framesToCopy    = Math.Min(framesRequested - framesWritten, _currentBuffer.Remaining);
            var sourceSpan      = _currentBuffer.Data.Span.Slice(_currentBuffer.Position, framesToCopy);
            var destinationSpan = _frameBuffer.AsSpan(framesWritten, framesToCopy);
            sourceSpan.CopyTo(destinationSpan);
            _currentBuffer.Advance(framesToCopy);
            framesWritten += framesToCopy;

            if ( _currentBuffer.IsFinished )
            {
                progressWriter.TryWrite(new AudioChunkPlaybackEndedEvent(_sessionId, turnId, DateTimeOffset.UtcNow, _currentBuffer.Segment));
                _framesOfChunkPlayed = 0;
                _currentBuffer       = null;
            }
        }

        Marshal.Copy(_frameBuffer, 0, output, framesRequested);

        _framesOfChunkPlayed += framesRequested;
        var currentTime = TimeSpan.FromSeconds((double)_framesOfChunkPlayed / DefaultSampleRate);
        progressWriter.TryWrite(new AudioPlaybackProgressEvent(_sessionId, turnId, DateTimeOffset.UtcNow, currentTime));

        if ( _currentBuffer == null && _producerCompleted )
        {
            completionSource?.TrySetResult(true);
            if ( _currentBuffer != null )
            {
                progressWriter.TryWrite(new AudioChunkPlaybackEndedEvent(_sessionId, turnId, DateTimeOffset.UtcNow, _currentBuffer.Segment));
            }

            progressWriter.TryComplete();

            return StreamCallbackResult.Complete;
        }

        if ( ct.IsCancellationRequested )
        {
            completionSource?.TrySetResult(false);
            if ( _currentBuffer != null )
            {
                progressWriter.TryWrite(new AudioChunkPlaybackEndedEvent(_sessionId, turnId, DateTimeOffset.UtcNow, _currentBuffer.Segment));
            }

            progressWriter.TryComplete();

            return StreamCallbackResult.Complete;
        }

        return StreamCallbackResult.Continue;

        void FillSilence()
        {
            if ( framesWritten < framesRequested )
            {
                Array.Clear(_frameBuffer, framesWritten, framesRequested - framesWritten);
            }

            Marshal.Copy(_frameBuffer, 0, output, framesRequested);
        }
    }

    private sealed class AudioBuffer(Memory<float> data, AudioSegment segment)
    {
        public Memory<float> Data { get; } = data;

        public AudioSegment Segment { get; } = segment;

        public int Position { get; private set; } = 0;

        public int Remaining => Data.Length - Position;

        public bool IsFinished => Position >= Data.Length;

        public void Advance(int count) { Position += count; }
    }
}

using System.Drawing;
using System.Numerics;

using FontStashSharp;

using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.UI.Text.Rendering;

using Silk.NET.Input;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

using Shader = PersonaEngine.Lib.UI.Common.Shader;

namespace PersonaEngine.Lib.UI.RouletteWheel;

public partial class RouletteWheel : IRenderComponent
{
    private const int MAX_VERTICES = 64;

    private const int MAX_INDICES = MAX_VERTICES * 6 / 4;

    private const float OUTER_RADIUS_FACTOR = 0.8f;

    private const float INNER_RADIUS_FACTOR = 0.64f; // OUTER_RADIUS_FACTOR * 0.8f

    private const float CENTER_RADIUS_FACTOR = 0.128f; // INNER_RADIUS_FACTOR * 0.2f

    private static readonly short[] _indexData = GenerateIndexArray();

    private readonly Lock _applyingConfig = new();

    private readonly IOptionsMonitor<RouletteWheelOptions> _config;

    private readonly FontProvider _fontProvider;

    private readonly Dictionary<int, int> _segmentFontSizes = new();

    private readonly float _textSizeAdaptationFactor = 1.0f;

    private readonly VertexPositionTexture[] _vertexData = new VertexPositionTexture[MAX_VERTICES];

    private GL _gl;

    private BufferObject<short> _indexBuffer;

    private float _minRotations;

    private int _numSections = 0;

    private Action<int>? _onSpinCompleteCallback;

    private Vector2 _position = Vector2.Zero;

    private bool _radialTextOrientation = true;

    private string[] _sectionLabels;

    private Shader _shader;

    private float _spinDuration;

    private float _spinStartTime = 0f;

    private float _startSegment = 0f;

    private float _targetSegment = 0f;

    private FSColor _textColor = new(255, 255, 255, 255);

    private TextRenderer _textRenderer;

    private float _textScale = 1.0f;

    private int _textStrokeWidth = 2;

    private float _time = 0f;

    private bool _useAdaptiveTextSize = true;

    private VertexArrayObject _vao;

    private BufferObject<VertexPositionTexture> _vertexBuffer;

    private int _vertexIndex = 0;

    private int _viewportHeight;

    private int _viewportWidth;

    private float _wheelSize = 1;

    public RouletteWheel(IOptionsMonitor<RouletteWheelOptions> config, FontProvider fontProvider)
    {
        _config       = config;
        _fontProvider = fontProvider;
    }

    public bool IsSpinning { get; private set; } = false;

    public float Progress { get; private set; } = 1f;

    public int NumberOfSections
    {
        get => _numSections;
        private set
        {
            _numSections   = Math.Clamp(value, 2, 24);
            _targetSegment = Math.Min(_targetSegment, _numSections - 1);
            _startSegment  = Math.Min(_startSegment, _numSections - 1);
            ResizeSectionLabels();

            if ( _useAdaptiveTextSize )
            {
                CalculateAllSegmentFontSizes();
            }
        }
    }

    public bool UseSpout => true;

    public string SpoutTarget => "RouletteWheel";

    public int Priority => 0;

    public void Initialize(GL gl, IView view, IInputContext input)
    {
        _gl           = gl;
        _textRenderer = new TextRenderer(gl);

        // Initialize buffers
        _vertexBuffer = new BufferObject<VertexPositionTexture>(gl, MAX_VERTICES, BufferTargetARB.ArrayBuffer, true);
        _indexBuffer  = new BufferObject<short>(gl, _indexData.Length, BufferTargetARB.ElementArrayBuffer, false);
        _indexBuffer.SetData(_indexData, 0, _indexData.Length);

        // Initialize shader
        var vertSrc = File.ReadAllText(Path.Combine(@"Resources/Shaders", "wheel_shader.vert"));
        var fragSrc = File.ReadAllText(Path.Combine(@"Resources/Shaders", "wheel_shader.frag"));
        _shader = new Shader(_gl, vertSrc, fragSrc);

        // Setup VAO
        unsafe
        {
            _vao = new VertexArrayObject(gl, sizeof(VertexPositionTexture));
            _vao.Bind();
        }

        var location = _shader.GetAttribLocation("a_position");
        _vao.VertexAttribPointer(location, 3, VertexAttribPointerType.Float, false, 0);

        location = _shader.GetAttribLocation("a_texCoords0");
        _vao.VertexAttribPointer(location, 2, VertexAttribPointerType.Float, false, 12);

        ResizeSectionLabels();

        ApplyConfiguration(_config.CurrentValue);

        _config.OnChange(ApplyConfiguration);

        // Prevent wheel from spinning on creation
        _spinStartTime = -_spinDuration;
    }

    public void Update(float deltaTime)
    {
        _time += deltaTime;

        if ( IsSpinning )
        {
            var progress = Math.Min((_time - _spinStartTime) / _spinDuration, 1.0f);
            Progress = progress;
        }

        if ( IsSpinning && _time - _spinStartTime >= _spinDuration )
        {
            IsSpinning = false;
            _onSpinCompleteCallback?.Invoke((int)_targetSegment);
            _onSpinCompleteCallback = null;
        }

        UpdateVisibilityAnimation();
    }

    public void Render(float deltaTime)
    {
        lock (_applyingConfig)
        {
            // Skip rendering if wheel is disabled and not animating
            if ( !IsEnabled && CurrentAnimationState == AnimationState.Idle )
            {
                return;
            }

            var originalWheelSize = _wheelSize;
            _wheelSize *= _animationCurrentScale;

            Begin();
            DrawWheel();
            End();

            // Only render labels if wheel is sufficiently visible
            if ( _animationCurrentScale > 0.25f )
            {
                RenderSectionLabels();
            }

            _wheelSize = originalWheelSize;
        }
    }

    public void Dispose()
    {
        // Context is destroyed anyway when app closes.

        return;

        _vao.Dispose();
        _vertexBuffer.Dispose();
        _indexBuffer.Dispose();
        _shader.Dispose();
        _textRenderer.Dispose();
    }

    public string GetLabel(int index) { return _sectionLabels[index]; }

    public string[] GetLabels() { return _sectionLabels.ToArray(); }

    public void Spin(int targetSection, Action<int>? onSpinComplete = null)
    {
        if ( IsSpinning || !IsEnabled || CurrentAnimationState != AnimationState.Idle )
        {
            return;
        }

        _targetSegment          = Math.Clamp(targetSection, 0, _numSections - 1);
        _startSegment           = GetCurrentWheelPosition();
        _spinStartTime          = _time;
        IsSpinning              = true;
        _onSpinCompleteCallback = onSpinComplete;
        Progress                = 0;
    }

    public int SpinRandom(Action<int>? onSpinComplete = null)
    {
        if ( IsSpinning || !IsEnabled || CurrentAnimationState != AnimationState.Idle )
        {
            return -1;
        }

        var target = Random.Shared.Next(_numSections);
        Spin(target, onSpinComplete);

        return target;
    }

    public Task<int> SpinAsync(int? targetSection = null)
    {
        if ( IsSpinning || !IsEnabled || CurrentAnimationState != AnimationState.Idle )
        {
            return Task.FromResult(-1);
        }

        var tcs    = new TaskCompletionSource<int>();
        var target = targetSection ?? Random.Shared.Next(_numSections);

        Spin(target, result => tcs.SetResult(result));

        return tcs.Task;
    }

    public void SetSectionLabels(string[] labels)
    {
        if ( labels.Length == 0 )
        {
            return;
        }

        if ( labels.Length != _numSections )
        {
            NumberOfSections = labels.Length;
        }

        for ( var i = 0; i < _numSections; i++ )
        {
            _sectionLabels[i] = labels[i];
        }

        if ( _useAdaptiveTextSize )
        {
            CalculateAllSegmentFontSizes();
        }
    }

    public void ConfigureTextStyle(
        bool   radialText      = true,
        Color? textColor       = null,
        float  scale           = 1.0f,
        int    strokeWidth     = 2,
        bool   useAdaptiveSize = true)
    {
        var color = textColor ?? Color.White;

        _textColor             = new FSColor(color.R, color.G, color.B, color.A);
        _textScale             = scale;
        _textStrokeWidth       = strokeWidth;
        _radialTextOrientation = radialText;
        _useAdaptiveTextSize   = useAdaptiveSize;

        if ( _useAdaptiveTextSize )
        {
            CalculateAllSegmentFontSizes();
        }
    }

    private static short[] GenerateIndexArray()
    {
        var result = new short[MAX_INDICES];
        for ( int i = 0,
                  vi = 0; i < MAX_INDICES; i += 6, vi += 4 )
        {
            result[i]     = (short)vi;
            result[i + 1] = (short)(vi + 1);
            result[i + 2] = (short)(vi + 2);
            result[i + 3] = (short)(vi + 1);
            result[i + 4] = (short)(vi + 3);
            result[i + 5] = (short)(vi + 2);
        }

        return result;
    }

    private void Begin()
    {
        _gl.Disable(EnableCap.DepthTest);
        _gl.Enable(EnableCap.Blend);
        _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);

        _shader.Use();
        UpdateShaderParameters();

        var projectionMatrix = Matrix4x4.CreateOrthographicOffCenter(0, _viewportWidth, _viewportHeight, 0, -1, 1);
        _shader.SetUniform("MatrixTransform", projectionMatrix);

        _vao.Bind();
        _vertexBuffer.Bind();
        _indexBuffer.Bind();
    }

    private void End() { FlushBuffer(); }

    private unsafe void FlushBuffer()
    {
        if ( _vertexIndex == 0 )
        {
            return;
        }

        _vertexBuffer.SetData(_vertexData, 0, _vertexIndex);
        _gl.DrawElements(PrimitiveType.Triangles, (uint)(_vertexIndex * 6 / 4), DrawElementsType.UnsignedShort, null);
        _vertexIndex = 0;
    }

    private void DrawWheel()
    {
        // Cache these calculations for performance
        float minDimension = Math.Min(_viewportWidth, _viewportHeight);
        var   radius       = minDimension * _wheelSize * OUTER_RADIUS_FACTOR / 2.0f;
        var   centerX      = _position.X;
        var   centerY      = _position.Y;
        var   cosR         = (float)Math.Cos(Rotation);
        var   sinR         = (float)Math.Sin(Rotation);
        var   left         = centerX - radius;
        var   right        = centerX + radius;
        var   top          = centerY - radius;
        var   bottom       = centerY + radius;

        // Top-left vertex
        _vertexData[_vertexIndex++] = new VertexPositionTexture(
                                                                RotatePoint(new Vector3(left, top, 0), centerX, centerY, cosR, sinR),
                                                                new Vector2(0, 0));

        // Top-right vertex
        _vertexData[_vertexIndex++] = new VertexPositionTexture(
                                                                RotatePoint(new Vector3(right, top, 0), centerX, centerY, cosR, sinR),
                                                                new Vector2(1, 0));

        // Bottom-left vertex
        _vertexData[_vertexIndex++] = new VertexPositionTexture(
                                                                RotatePoint(new Vector3(left, bottom, 0), centerX, centerY, cosR, sinR),
                                                                new Vector2(0, 1));

        // Bottom-right vertex
        _vertexData[_vertexIndex++] = new VertexPositionTexture(
                                                                RotatePoint(new Vector3(right, bottom, 0), centerX, centerY, cosR, sinR),
                                                                new Vector2(1, 1));
    }

    private void RenderSectionLabels()
    {
        // Cache these calculations for performance
        float minDimension         = Math.Min(_viewportWidth, _viewportHeight);
        var   radius               = minDimension * _wheelSize * OUTER_RADIUS_FACTOR / 2.0f;
        var   innerRadius          = radius * INNER_RADIUS_FACTOR;
        var   centerRadius         = radius * CENTER_RADIUS_FACTOR;
        var   centerX              = _position.X != 0 ? _position.X : _viewportWidth / 2.0f;
        var   centerY              = _position.Y != 0 ? _position.Y : _viewportHeight / 2.0f;
        var   availableRadialSpace = innerRadius - centerRadius;
        var   textRadius           = centerRadius + availableRadialSpace * 0.5f;
        var   currentRotation      = GetCurrentWheelRotation();
        var   segmentAngle         = 2.0f * MathF.PI / _numSections;

        if ( _useAdaptiveTextSize && _segmentFontSizes.Count != _numSections )
        {
            CalculateAllSegmentFontSizes();
        }

        _textRenderer.Begin();

        var fontSystem = _fontProvider.GetFontSystem(_config.CurrentValue.Font);

        // Reusable Vector2 for text position to avoid allocation in the loop
        var position = new Vector2();

        for ( var i = 0; i < _numSections; i++ )
        {
            if ( string.IsNullOrEmpty(_sectionLabels[i]) )
            {
                continue;
            }

            var fontSize    = _useAdaptiveTextSize ? _segmentFontSizes[i] : _config.CurrentValue.FontSize;
            var segmentFont = fontSystem.GetFont(fontSize);

            var rotatedStartAngle = -currentRotation;
            var rotatedEndAngle   = segmentAngle - currentRotation;
            var segmentCenter     = (rotatedStartAngle + rotatedEndAngle) / 2.0f + (_numSections - 1 - i) * segmentAngle;

            // Position text
            var textX    = centerX + textRadius * MathF.Cos(segmentCenter);
            var textY    = centerY + textRadius * MathF.Sin(segmentCenter);
            var textSize = segmentFont.MeasureString(_sectionLabels[i]);

            // Update position instead of creating a new Vector2
            position.X = textX;
            position.Y = textY;

            // Calculate text rotation
            var textRotation = _radialTextOrientation
                                   ? MathF.Atan2(textY - centerY, textX - centerX)
                                   : 0.0f;

            // Draw text
            segmentFont.DrawText(
                                 _textRenderer,
                                 _sectionLabels[i],
                                 position,
                                 _textColor,
                                 textRotation,
                                 scale: new Vector2(_textScale),
                                 effect: FontSystemEffect.Stroked,
                                 effectAmount: _textStrokeWidth,
                                 origin: textSize / 2);
        }

        _textRenderer.End();
    }

    private Vector3 RotatePoint(Vector3 point, float centerX, float centerY, float cosR, float sinR)
    {
        var x = point.X - centerX;
        var y = point.Y - centerY;

        var newX = x * cosR - y * sinR + centerX;
        var newY = x * sinR + y * cosR + centerY;

        return new Vector3(newX, newY, point.Z);
    }

    private float GetCurrentWheelPosition()
    {
        if ( !IsSpinning )
        {
            return _targetSegment;
        }

        if ( Progress >= 1.0f )
        {
            IsSpinning = false;

            return _targetSegment;
        }

        var easedProgress = EaseOutQuint(Progress);

        var segmentDiff = (_targetSegment - _startSegment + _numSections) % _numSections;
        if ( segmentDiff > _numSections / 2.0f )
        {
            segmentDiff -= _numSections;
        }

        return (_startSegment + segmentDiff * easedProgress + _numSections) % _numSections;
    }

    private float GetCurrentWheelRotation()
    {
        var baseRotation = -Rotation;
        var segmentAngle = 2.0f * MathF.PI / _numSections;

        var targetAngle = baseRotation + -(_targetSegment + 0.5f) * segmentAngle + MathF.PI * 1.5f;

        if ( !IsSpinning )
        {
            return targetAngle;
        }

        var startAngle    = baseRotation + -(_startSegment + 0.5f) * segmentAngle + MathF.PI * 1.5f;
        var easedProgress = EaseOutQuint(Progress);

        var adjustedExtraAngle = targetAngle - startAngle;
        if ( adjustedExtraAngle > 0.0 )
        {
            adjustedExtraAngle -= 2.0f * MathF.PI;
        }

        var spinningAngle = startAngle + (adjustedExtraAngle - _minRotations * 2.0f * MathF.PI) * easedProgress;

        return spinningAngle;
    }

    private void UpdateShaderParameters()
    {
        _shader.SetUniform("u_targetSegment", _targetSegment);
        _shader.SetUniform("u_startSegment", _startSegment);
        _shader.SetUniform("u_spinDuration", _spinDuration);
        _shader.SetUniform("u_minRotations", _minRotations);
        _shader.SetUniform("u_spinStartTime", _spinStartTime);
        _shader.SetUniform("u_time", _time);
        _shader.SetUniform("u_numSlices", (float)_numSections);

        float size = Math.Min(_viewportWidth, _viewportHeight);
        _shader.SetUniform("u_resolution", size, size);
    }

    private int CalculateOptimalTextSizeForSegment(string label, int segmentIndex)
    {
        if ( string.IsNullOrEmpty(label) )
        {
            return 24;
        }

        // Cache these calculations for performance
        float minDimension         = Math.Min(_viewportWidth, _viewportHeight);
        var   radius               = minDimension * _wheelSize * OUTER_RADIUS_FACTOR / 2.0f;
        var   innerRadius          = radius * INNER_RADIUS_FACTOR;
        var   centerRadius         = radius * CENTER_RADIUS_FACTOR;
        var   availableRadialSpace = innerRadius - centerRadius;
        var   textRadius           = centerRadius + availableRadialSpace * 0.5f;
        var   segmentAngle         = 2.0f * MathF.PI / _numSections;
        var   maxWidth             = 2.0f * textRadius * MathF.Sin(segmentAngle / 2.0f);

        var fontSystem = _fontProvider.GetFontSystem(_config.CurrentValue.Font);

        // Binary search for optimal font size
        var minFontSize     = 1;
        var maxFontSize     = 72;
        var optimalFontSize = _config.CurrentValue.FontSize;

        while ( minFontSize <= maxFontSize )
        {
            var currentFontSize = (minFontSize + maxFontSize) / 2;

            var testFont    = fontSystem.GetFont(currentFontSize);
            var textSize    = testFont.MeasureString(label);
            var scaledWidth = textSize.X * _textScale * _textSizeAdaptationFactor;

            if ( scaledWidth <= maxWidth * INNER_RADIUS_FACTOR )
            {
                optimalFontSize = currentFontSize;
                minFontSize     = currentFontSize + 1;
            }
            else
            {
                maxFontSize = currentFontSize - 1;
            }
        }

        return optimalFontSize;
    }

    private void CalculateAllSegmentFontSizes()
    {
        if ( !_useAdaptiveTextSize )
        {
            return;
        }

        _segmentFontSizes.Clear();

        for ( var i = 0; i < _numSections; i++ )
        {
            _segmentFontSizes[i] = string.IsNullOrEmpty(_sectionLabels[i])
                                       ? 24
                                       : CalculateOptimalTextSizeForSegment(_sectionLabels[i], i);
        }
    }

    private void ResizeSectionLabels()
    {
        var newLabels = new string[_numSections];
        for ( var i = 0; i < _numSections; i++ )
        {
            newLabels[i] = i < _sectionLabels?.Length ? _sectionLabels[i] : $"Section {i + 1}";
        }

        _sectionLabels = newLabels;
    }

    private static float EaseOutQuint(float t) { return 1.0f - MathF.Pow(1.0f - t, 5); }

    private void ApplyConfiguration(RouletteWheelOptions options)
    {
        lock (_applyingConfig)
        {
            // Apply text configuration
            _radialTextOrientation = options.RadialTextOrientation;
            _textScale             = options.TextScale;
            _textStrokeWidth       = options.TextStroke;
            _useAdaptiveTextSize   = options.AdaptiveText;

            if ( !string.IsNullOrEmpty(options.TextColor) )
            {
                var color = ColorTranslator.FromHtml(options.TextColor);
                _textColor = new FSColor(color.R, color.G, color.B, color.A);
            }

            // Apply section labels
            if ( options.SectionLabels?.Length > 0 )
            {
                SetSectionLabels(options.SectionLabels);
            }

            // Apply spin parameters
            _spinDuration = Math.Max(options.SpinDuration, 0.1f);
            _minRotations = Math.Max(options.MinRotations, 1.0f);

            // Apply wheel size
            _wheelSize = Math.Clamp(options.WheelSizePercentage, 0.1f, 1.0f);

            // Apply positioning
            if ( !string.IsNullOrEmpty(options.PositionMode) )
            {
                switch ( options.PositionMode )
                {
                    case "Absolute":
                        _positionMode = PositionMode.Absolute;
                        SetAbsolutePosition(options.AbsolutePositionX, options.AbsolutePositionY);

                        break;
                    case "Percentage":
                        _positionMode = PositionMode.Percentage;
                        PositionByPercentage(options.PositionXPercentage, options.PositionYPercentage);

                        break;
                    case "Anchored":
                        _positionMode = PositionMode.Anchored;
                        if ( Enum.TryParse(options.ViewportAnchor, out ViewportAnchor anchor) )
                        {
                            PositionAt(anchor, new Vector2(options.AnchorOffsetX, options.AnchorOffsetY));
                        }
                        else
                        {
                            PositionAt(ViewportAnchor.Center);
                        }

                        break;
                }
            }

            // Apply rotation
            Rotation = options.RotationDegrees * MathF.PI / 180;

            // Handle viewport size changes
            var viewportChanged = _viewportWidth != options.Width || _viewportHeight != options.Height;
            if ( viewportChanged )
            {
                _viewportWidth  = options.Width;
                _viewportHeight = options.Height;

                // Update text renderer viewport
                _textRenderer.OnViewportChanged(_viewportWidth, _viewportHeight);

                // Update position based on mode
                switch ( _positionMode )
                {
                    case PositionMode.Anchored:
                        UpdatePositionFromAnchor();

                        break;
                    case PositionMode.Percentage:
                        UpdatePositionFromPercentage();

                        break;
                }
            }

            // Apply enabled state (if changed)
            if ( options.Enabled != IsEnabled )
            {
                if ( options.Enabled )
                {
                    Enable();
                }
                else
                {
                    Disable();
                }
            }

            // Recalculate font sizes if needed (after all other changes)
            if ( _useAdaptiveTextSize )
            {
                CalculateAllSegmentFontSizes();
            }
        }
    }
}

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Espeak-based fallback phonemizer matching the Python reference implementation
/// </summary>
public class EspeakFallbackPhonemizer : IFallbackPhonemizer
{
    // Dictionary of espeak to model phoneme mappings, sorted by key length descending (matching Python E2M)
    private static readonly IReadOnlyDictionary<string, string> E2M = new Dictionary<string, string> {
                                                                                { "ʔˌn\u0329", "tn" },
                                                                                { "ʔn\u0329", "tn" },
                                                                                { "ʔn", "tn" },
                                                                                { "ʔ", "t" },
                                                                                { "a^ɪ", "I" },
                                                                                { "a^ʊ", "W" },
                                                                                { "d^ʒ", "ʤ" },
                                                                                { "e^ɪ", "A" },
                                                                                { "e", "A" },
                                                                                { "t^ʃ", "ʧ" },
                                                                                { "ɔ^ɪ", "Y" },
                                                                                { "ə^l", "ᵊl" },
                                                                                { "ʲo", "jo" },
                                                                                { "ʲə", "jə" },
                                                                                { "ʲ", "" },
                                                                                { "ɚ", "əɹ" },
                                                                                { "r", "ɹ" },
                                                                                { "x", "k" },
                                                                                { "ç", "k" },
                                                                                { "ɐ", "ə" },
                                                                                { "ɬ", "l" },
                                                                                { "\u0303", "" }
                                                                                }.OrderByDescending(kv => kv.Key.Length).ToDictionary(kv => kv.Key, kv => kv.Value);

    // Add a cache to avoid repeated processing
    private readonly ConcurrentDictionary<string, (string? Phonemes, int? Rating)> _cache = new();

    private readonly ILogger<EspeakFallbackPhonemizer> _logger;

    private readonly SemaphoreSlim _processLock = new(1, 1);

    private readonly int _processReadTimeoutMs = 2000;

    private readonly int _processStartTimeoutMs = 5000;

    private readonly IOptionsMonitor<TtsConfiguration> _ttsConfig;

    private readonly IDisposable? _ttsConfigChangeToken;

    private readonly IOptionsMonitor<KokoroVoiceOptions> _voiceOptions;

    private readonly IDisposable? _voiceOptionsChangeToken;

    private bool _disposed;

    private volatile string _espeakPath;

    // Process state
    private Process? _espeakProcess;

    private volatile bool _needProcessReset;

    private volatile bool _useBritishEnglish;

    public EspeakFallbackPhonemizer(
        IOptionsMonitor<TtsConfiguration>   ttsConfig,
        IOptionsMonitor<KokoroVoiceOptions> voiceOptions,
        ILogger<EspeakFallbackPhonemizer>   logger)
    {
        _ttsConfig    = ttsConfig ?? throw new ArgumentNullException(nameof(ttsConfig));
        _voiceOptions = voiceOptions ?? throw new ArgumentNullException(nameof(voiceOptions));
        _logger       = logger ?? throw new ArgumentNullException(nameof(logger));

        _espeakPath        = _ttsConfig.CurrentValue.EspeakPath ?? throw new ArgumentNullException(nameof(ttsConfig.CurrentValue.EspeakPath));
        _useBritishEnglish = _voiceOptions.CurrentValue.UseBritishEnglish;

        _voiceOptionsChangeToken = _voiceOptions.OnChange(options =>
                                                          {
                                                              _useBritishEnglish = options.UseBritishEnglish;
                                                              _logger.LogDebug("Voice options updated: UseBritishEnglish={UseBritishEnglish}", _useBritishEnglish);
                                                              _needProcessReset = true;

                                                              // Clear cache when voice options change
                                                              _cache.Clear();
                                                          });

        _ttsConfigChangeToken = _ttsConfig.OnChange(config =>
                                                    {
                                                        if ( string.IsNullOrEmpty(config.EspeakPath) || _espeakPath == config.EspeakPath )
                                                        {
                                                            return;
                                                        }

                                                        _espeakPath = config.EspeakPath;
                                                        _logger.LogDebug("TTS configuration updated: EspeakPath={EspeakPath}", _espeakPath);
                                                        _needProcessReset = true;

                                                        // Clear cache when espeak path changes
                                                        _cache.Clear();
                                                    });
    }

    /// <summary>
    ///     Gets phonemes for a word using espeak-ng command-line tool
    /// </summary>
    public async Task<(string? Phonemes, int? Rating)> GetPhonemesAsync(
        string            word,
        CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(word) )
        {
            return (null, null);
        }

        // Check cache first
        if ( _cache.TryGetValue(word, out var cachedResult) )
        {
            return cachedResult;
        }

        var cancelled = false;

        try
        {
            await _processLock.WaitAsync(cancellationToken);

            // Check cache again in case another thread added the result while we were waiting
            if ( _cache.TryGetValue(word, out cachedResult) )
            {
                return cachedResult;
            }

            var cleanWord = SanitizeInput(word);

            // Ensure the process is running
            var process = await EnsureProcessIsInitializedAsync(cancellationToken);

            // Write the word to the process stdin
            await process.StandardInput.WriteLineAsync(cleanWord);
            await process.StandardInput.FlushAsync();

            // Read the output with a timeout
            var readTask = process.StandardOutput.ReadLineAsync();

            if ( await Task.WhenAny(readTask, Task.Delay(_processReadTimeoutMs, cancellationToken)) != readTask )
            {
                _logger.LogWarning("Timeout reading output from espeak-ng for word {Word}", word);
                _needProcessReset = true;

                return (null, null);
            }

            var output = await readTask;

            if ( string.IsNullOrEmpty(output) )
            {
                _logger.LogDebug("Empty output from espeak-ng for word {Word}", word);
                _needProcessReset = true;

                return (null, null);
            }

            var phonemes = NormalizeEspeakOutput(output.Trim(), _useBritishEnglish);

            var result = (phonemes, 2);

            // Add to cache
            _cache[word] = result;

            return result;
        }
        catch (OperationCanceledException)
        {
            cancelled = true;

            return (null, null);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting phonemes from espeak-ng for word {Word}", word);
            _needProcessReset = true;

            return (null, null);
        }
        finally
        {
            if ( !cancelled )
            {
                _processLock.Release();
            }
        }
    }

    public async ValueTask DisposeAsync()
    {
        if ( _disposed )
        {
            return;
        }

        _voiceOptionsChangeToken?.Dispose();
        _ttsConfigChangeToken?.Dispose();

        if ( _espeakProcess != null )
        {
            try
            {
                if ( !_espeakProcess.HasExited )
                {
                    _espeakProcess.Kill();
                }

                _espeakProcess.Dispose();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error disposing espeak-ng process");
            }
        }

        _processLock.Dispose();
        _disposed = true;

        await Task.CompletedTask;
    }

    /// <summary>
    ///     Ensures the espeak process is initialized and running
    /// </summary>
    private async Task<Process> EnsureProcessIsInitializedAsync(CancellationToken cancellationToken)
    {
        // If process needs to be reset or doesn't exist or has exited, create a new one
        if ( _needProcessReset || _espeakProcess == null || _espeakProcess.HasExited )
        {
            // Dispose of the old process if it exists
            if ( _espeakProcess != null )
            {
                try
                {
                    if ( !_espeakProcess.HasExited )
                    {
                        _espeakProcess.Kill();
                    }

                    _espeakProcess.Dispose();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error disposing espeak-ng process");
                }

                _espeakProcess = null;
            }

            // Create a new process
            var language = _useBritishEnglish ? "en-gb" : "en-us";
            var startInfo = new ProcessStartInfo {
                                                     FileName               = _espeakPath,
                                                     Arguments              = $"--ipa=1 -v {language} -q --tie=^",
                                                     RedirectStandardOutput = true,
                                                     RedirectStandardInput  = true,
                                                     StandardOutputEncoding = Encoding.UTF8,
                                                     StandardInputEncoding  = Encoding.UTF8,
                                                     UseShellExecute        = false,
                                                     CreateNoWindow         = true
                                                 };

            var process = new Process { StartInfo = startInfo };

            // Start the process with a timeout
            var startTask = Task.Run(() =>
                                     {
                                         process.Start();

                                         return true;
                                     });

            if ( await Task.WhenAny(startTask, Task.Delay(_processStartTimeoutMs, cancellationToken)) != startTask )
            {
                try
                {
                    if ( !process.HasExited )
                    {
                        process.Kill();
                    }

                    process.Dispose();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error disposing espeak-ng process after start timeout");
                }

                throw new TimeoutException("Timeout starting espeak-ng process");
            }

            if ( !startTask.Result )
            {
                throw new InvalidOperationException("Failed to start espeak-ng process");
            }

            _espeakProcess    = process;
            _needProcessReset = false;
            _logger.LogDebug("Started new espeak-ng process");
        }

        return _espeakProcess;
    }

    /// <summary>
    ///     Sanitizes input for espeak command line
    /// </summary>
    private string SanitizeInput(string input)
    {
        // Remove characters that could cause command line issues
        return input.Replace("\"", "")
                    .Replace("\\", "")
                    .Replace(";", "")
                    .Replace("&", "")
                    .Replace("|", "")
                    .Replace(">", "")
                    .Replace("<", "")
                    .Replace("`", "");
    }

    /// <summary>
    ///     Normalizes espeak output to match the Python implementation
    /// </summary>
    private string NormalizeEspeakOutput(string output, bool useBritishEnglish)
    {
        // Apply all replacements from E2M dictionary
        var result = output;

        // Apply all the E2M replacements in order of key length (longest first)
        foreach ( var kvp in E2M )
        {
            result = result.Replace(kvp.Key, kvp.Value);
        }

        // Apply the regex substitution similar to the Python version
        // This converts characters with combining subscript (U+0329) to have a schwa prefix
        result = Regex.Replace(result, @"(\S)\u0329", "ᵊ$1");
        result = result.Replace("\u0329", "");

        // Apply language-specific replacements as in the Python implementation
        if ( useBritishEnglish )
        {
            result = result.Replace("e^ə", "ɛː");
            result = result.Replace("iə", "ɪə");
            result = result.Replace("ə^ʊ", "Q");
        }
        else // American English
        {
            result = result.Replace("o^ʊ", "O");
            result = result.Replace("ɜːɹ", "ɜɹ");
            result = result.Replace("ɜː", "ɜɹ");
            result = result.Replace("ɪə", "iə");
            result = result.Replace("ː", "");
        }

        // Common replacement for both language variants
        result = result.Replace("o", "ɔ"); // for espeak < 1.52

        // Final clean-up - remove tie character and spaces
        return result.Replace("^", "").Replace(" ", "");
    }
}

using System.Collections.Concurrent;

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.TTS.RVC;

public class RVCVoiceProvider : IRVCVoiceProvider
{
    private readonly ILogger<RVCVoiceProvider> _logger;

    private readonly IModelProvider _modelProvider;

    private readonly ConcurrentDictionary<string, string> _voicePaths = new();

    private bool _disposed;

    public RVCVoiceProvider(
        IModelProvider            ittsModelProvider,
        ILogger<RVCVoiceProvider> logger)
    {
        _modelProvider = ittsModelProvider ?? throw new ArgumentNullException(nameof(ittsModelProvider));
        _logger        = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <inheritdoc />
    public async Task<string> GetVoiceAsync(
        string            voiceId,
        CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(voiceId) )
        {
            throw new ArgumentException("Voice ID cannot be null or empty", nameof(voiceId));
        }

        return await GetVoicePathAsync(voiceId, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<string>> GetAvailableVoicesAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            // Get voice directory
            var model     = await _modelProvider.GetModelAsync(Synthesis.ModelType.RVCVoices, cancellationToken);
            var voicesDir = model.Path;

            if ( !Directory.Exists(voicesDir) )
            {
                _logger.LogWarning("Voices directory not found: {Path}", voicesDir);

                return Array.Empty<string>();
            }

            // Get all .bin files
            var voiceFiles = Directory.GetFiles(voicesDir, "*.onnx");

            // Extract voice IDs from filenames
            var voiceIds = new List<string>(voiceFiles.Length);

            foreach ( var file in voiceFiles )
            {
                var voiceId = Path.GetFileNameWithoutExtension(file);
                voiceIds.Add(voiceId);

                // Cache the path for faster lookup
                _voicePaths[voiceId] = file;
            }

            _logger.LogInformation("Found {Count} available RVC voices", voiceIds.Count);

            return voiceIds;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting available voices");

            throw;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if ( _disposed )
        {
            return;
        }

        _voicePaths.Clear();
        _disposed = true;

        await Task.CompletedTask;
    }

    private async Task<string> GetVoicePathAsync(string voiceId, CancellationToken cancellationToken)
    {
        if ( _voicePaths.TryGetValue(voiceId, out var cachedPath) )
        {
            return cachedPath;
        }

        var model     = await _modelProvider.GetModelAsync(Synthesis.ModelType.RVCVoices, cancellationToken);
        var voicesDir = model.Path;

        var voicePath = Path.Combine(voicesDir, $"{voiceId}.onnx");

        if ( !File.Exists(voicePath) )
        {
            _logger.LogWarning("Voice file not found: {Path}", voicePath);

            throw new FileNotFoundException($"Voice file not found for {voiceId}", voicePath);
        }

        _voicePaths[voiceId] = voicePath;

        return voicePath;
    }
}

using System.Runtime.InteropServices;
using System.Text;

using Microsoft.Extensions.Logging;

using PortAudioSharp;

using Stream = PortAudioSharp.Stream;

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents a source that captures audio from a microphone.
/// </summary>
public sealed class MicrophoneInputPortAudioSource : AwaitableWaveFileSource, IMicrophone
{
    private readonly int _bitsPerSample;

    private readonly int _channels;

    private readonly CancellationTokenSource _cts;

    private readonly int _deviceNumber;

    private readonly uint _framesPerBuffer;

    private readonly ILogger _logger;

    private readonly int _sampleRate;

    private Stream? _audioStream;

    private bool _isRecording;

    private Dictionary<string, string> _metadata;

    public MicrophoneInputPortAudioSource(
        ILogger<MicrophoneInputPortAudioSource> logger,
        int                                     deviceNumber        = -1,
        int                                     sampleRate          = 16000,
        int                                     bitsPerSample       = 16,
        int                                     channels            = 1,
        uint                                    framesPerBuffer     = 0,
        bool                                    storeSamples        = true,
        bool                                    storeBytes          = false,
        int                                     initialSizeFloats   = DefaultInitialSize,
        int                                     initialSizeBytes    = DefaultInitialSize,
        IChannelAggregationStrategy?            aggregationStrategy = null)
        : this(new Dictionary<string, string>(), logger, deviceNumber, sampleRate, bitsPerSample,
               channels, framesPerBuffer, storeSamples, storeBytes, initialSizeFloats, initialSizeBytes, aggregationStrategy) { }

    private MicrophoneInputPortAudioSource(
        Dictionary<string, string>              metadata,
        ILogger<MicrophoneInputPortAudioSource> logger,
        int                                     deviceNumber        = -1,
        int                                     sampleRate          = 16000,
        int                                     bitsPerSample       = 16,
        int                                     channels            = 1,
        uint                                    framesPerBuffer     = 0,
        bool                                    storeSamples        = true,
        bool                                    storeBytes          = false,
        int                                     initialSizeFloats   = DefaultInitialSize,
        int                                     initialSizeBytes    = DefaultInitialSize,
        IChannelAggregationStrategy?            aggregationStrategy = null)
        : base(metadata, storeSamples, storeBytes, initialSizeFloats, initialSizeBytes, aggregationStrategy)
    {
        PortAudio.Initialize();

        _logger          = logger;
        _deviceNumber    = deviceNumber == -1 ? PortAudio.DefaultInputDevice : deviceNumber;
        _sampleRate      = sampleRate;
        _bitsPerSample   = bitsPerSample;
        _channels        = channels;
        _framesPerBuffer = framesPerBuffer;
        _cts             = new CancellationTokenSource();
        _metadata        = metadata;

        if ( _deviceNumber == PortAudio.NoDevice )
        {
            var sb = new StringBuilder();

            for ( var i = 0; i != PortAudio.DeviceCount; ++i )
            {
                var deviceInfo = PortAudio.GetDeviceInfo(i);

                sb.AppendLine($"[*] Device {i}");
                sb.AppendLine($"        Name: {deviceInfo.name}");
                sb.AppendLine($"        Max input channels: {deviceInfo.maxInputChannels}");
                sb.AppendLine($"        Default sample rate: {deviceInfo.defaultSampleRate}");
            }

            logger.LogWarning("Devices available: {Devices}", sb);

            throw new InvalidOperationException("No default input device available");
        }

        Initialize(new AudioSourceHeader { BitsPerSample = (ushort)bitsPerSample, Channels = (ushort)channels, SampleRate = (uint)sampleRate });

        try
        {
            InitializeAudioStream();
        }
        catch
        {
            PortAudio.Terminate();

            throw;
        }
    }

    public void StartRecording()
    {
        if ( _isRecording )
        {
            return;
        }

        _isRecording = true;
        _audioStream?.Start();
    }

    public void StopRecording()
    {
        if ( !_isRecording )
        {
            return;
        }

        _isRecording = false;
        _audioStream?.Stop();
        Flush();
    }

    public IEnumerable<string> GetAvailableDevices()
    {
        throw new NotImplementedException();
    }

    private void InitializeAudioStream()
    {
        var deviceInfo = PortAudio.GetDeviceInfo(_deviceNumber);
        _logger.LogInformation("Using input device: {DeviceName}", deviceInfo.name);

        var inputParams = new StreamParameters {
                                                   device                    = _deviceNumber,
                                                   channelCount              = _channels,
                                                   sampleFormat              = SampleFormat.Float32,
                                                   suggestedLatency          = deviceInfo.defaultLowInputLatency,
                                                   hostApiSpecificStreamInfo = IntPtr.Zero
                                               };

        _audioStream = new Stream(
                                  inputParams,
                                  null,
                                  _sampleRate,
                                  _framesPerBuffer,
                                  StreamFlags.ClipOff,
                                  ProcessAudioCallback,
                                  IntPtr.Zero);
    }

    private StreamCallbackResult ProcessAudioCallback(
        IntPtr                     input,
        IntPtr                     output,
        uint                       frameCount,
        ref StreamCallbackTimeInfo timeInfo,
        StreamCallbackFlags        statusFlags,
        IntPtr                     userData)
    {
        if ( _cts.Token.IsCancellationRequested )
        {
            return StreamCallbackResult.Complete;
        }

        try
        {
            var samples = new float[frameCount];
            Marshal.Copy(input, samples, 0, (int)frameCount);

            var byteArray = SampleSerializer.Serialize(samples, Header.BitsPerSample);

            WriteData(byteArray);

            return StreamCallbackResult.Continue;
        }
        catch
        {
            return StreamCallbackResult.Abort;
        }
    }

    protected override void Dispose(bool disposing)
    {
        if ( disposing )
        {
            _cts.Cancel();
            StopRecording();
            _audioStream?.Dispose();
            _audioStream = null;
            PortAudio.Terminate();
        }

        base.Dispose(disposing);
    }
}

#pragma warning disable SKEXP0001

using System.Diagnostics.CodeAnalysis;
using System.Globalization;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.ML.OnnxRuntime;
using Microsoft.SemanticKernel;

using PersonaEngine.Lib.ASR.Transcriber;
using PersonaEngine.Lib.ASR.VAD;
using PersonaEngine.Lib.Audio;
using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.Core;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.Core.Conversation.Implementations.Adapters.Audio.Input;
using PersonaEngine.Lib.Core.Conversation.Implementations.Adapters.Audio.Output;
using PersonaEngine.Lib.Core.Conversation.Implementations.Metrics;
using PersonaEngine.Lib.Core.Conversation.Implementations.Session;
using PersonaEngine.Lib.Live2D;
using PersonaEngine.Lib.Live2D.Behaviour;
using PersonaEngine.Lib.Live2D.Behaviour.Emotion;
using PersonaEngine.Lib.Live2D.Behaviour.LipSync;
using PersonaEngine.Lib.LLM;
using PersonaEngine.Lib.Logging;
using PersonaEngine.Lib.TTS.Audio;
using PersonaEngine.Lib.TTS.Profanity;
using PersonaEngine.Lib.TTS.RVC;
using PersonaEngine.Lib.TTS.Synthesis;
using PersonaEngine.Lib.UI;
using PersonaEngine.Lib.UI.Common;
using PersonaEngine.Lib.UI.GUI;
using PersonaEngine.Lib.UI.RouletteWheel;
using PersonaEngine.Lib.UI.Text.Subtitles;
using PersonaEngine.Lib.Vision;

using Polly;
using Polly.Retry;
using Polly.Timeout;

namespace PersonaEngine.Lib;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApp(this IServiceCollection services, IConfiguration configuration, Action<IKernelBuilder>? configureKernel = null)
    {
        services.Configure<AvatarAppConfig>(configuration.GetSection("Config"));

        services.AddConversation(configuration, configureKernel);
        services.AddUI(configuration);
        services.AddLive2D(configuration);
        services.AddSystemAudioPlayer();
        services.AddPolly(configuration);

        services.AddSingleton<AvatarApp>();

        OrtEnv.Instance().EnvLogLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR;

        return services;
    }

    public static IServiceCollection AddConversation(this IServiceCollection services, IConfiguration configuration, Action<IKernelBuilder>? configureKernel = null)
    {
        services.AddASRSystem(configuration);
        services.AddTTSSystem(configuration);
        services.AddRVC(configuration);
#pragma warning disable SKEXP0010
        services.AddLLM(configuration, configureKernel);
#pragma warning restore SKEXP0010
        services.AddChatEngineSystem(configuration);

        services.AddConversationPipeline(configuration);

        services.AddSingleton<ProfanityDetector>();

        return services;
    }

    public static IServiceCollection AddConversationPipeline(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSingleton<ConversationMetrics>();

        services.AddSingleton<IInputAdapter, MicrophoneInputAdapter>();

        services.AddSingleton<IConversationSessionFactory, ConversationSessionFactory>();
        services.AddSingleton<IConversationOrchestrator, ConversationOrchestrator>();

        return services;
    }

    public static IServiceCollection AddASRSystem(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<AsrConfiguration>(configuration.GetSection("Config:Asr"));
        services.Configure<MicrophoneConfiguration>(configuration.GetSection("Config:Microphone"));

        services.AddSingleton<IVadDetector>(sp =>
                                            {
                                                var asrOptions = sp.GetRequiredService<IOptions<AsrConfiguration>>().Value;

                                                var siletroOptions = new SileroVadOptions(ModelUtils.GetModelPath(ModelType.Silero)) { Threshold = asrOptions.VadThreshold, ThresholdGap = asrOptions.VadThresholdGap };

                                                var vadOptions = new VadDetectorOptions { MinSpeechDuration = TimeSpan.FromMilliseconds(asrOptions.VadMinSpeechDuration), MinSilenceDuration = TimeSpan.FromMilliseconds(asrOptions.VadMinSilenceDuration) };

                                                return new SileroVadDetector(vadOptions, siletroOptions);
                                            });

        services.AddSingleton<IRealtimeSpeechTranscriptor>(sp =>
                                                           {
                                                               var asrOptions = sp.GetRequiredService<IOptions<AsrConfiguration>>().Value;

                                                               var realtimeSpeechTranscriptorOptions = new RealtimeSpeechTranscriptorOptions {
                                                                                AutodetectLanguageOnce        = false,                    // Flag to detect the language only once or for each segment
                                                                                IncludeSpeechRecogizingEvents = true,                     // Flag to include speech recognizing events (RealtimeSegmentRecognizing)
                                                                                RetrieveTokenDetails          = false,                    // Flag to retrieve token details
                                                                                LanguageAutoDetect            = false,                    // Flag to auto-detect the language
                                                                                Language                      = new CultureInfo("en-US"), // Language to use for transcription
                                                                                Prompt                        = asrOptions.TtsPrompt,
                                                                                Template                      = asrOptions.TtsMode
                                                                                };

                                                               var realTimeOptions = new RealtimeOptions();

                                                               return new RealtimeTranscriptor(
                                                                                new WhisperSpeechTranscriptorFactory(ModelUtils.GetModelPath(ModelType.WhisperGgmlTurbov3)),
                                                                                sp.GetRequiredService<IVadDetector>(),
                                                                                new WhisperSpeechTranscriptorFactory(ModelUtils.GetModelPath(ModelType.WhisperGgmlTiny)),
                                                                                realtimeSpeechTranscriptorOptions,
                                                                                realTimeOptions,
                                                                                sp.GetRequiredService<ILogger<RealtimeTranscriptor>>());
                                                           });

        services.AddSingleton<IMicrophone, MicrophoneInputNAudioSource>();
        services.AddSingleton<IAwaitableAudioSource>(sp => sp.GetRequiredService<IMicrophone>());

        return services;
    }

    public static IServiceCollection AddSystemAudioPlayer(this IServiceCollection services)
    {
        services.AddSingleton<IOutputAdapter, PortaudioOutputAdapter>();
        services.AddSingleton<IAudioProgressNotifier, AudioProgressNotifier>();

        return services;
    }

    public static IServiceCollection AddChatEngineSystem(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<ConversationOptions>(configuration.GetSection("Config:Conversation"));
        services.Configure<ConversationContextOptions>(configuration.GetSection("Config:ConversationContext"));

        services.AddSingleton<IChatEngine, SemanticKernelChatEngine>();
        services.AddSingleton<IVisualChatEngine, VisualQASemanticKernelChatEngine>();
        services.AddSingleton<IVisualQAService, VisualQAService>();
        services.AddSingleton<WindowCaptureService>();

        return services;
    }

    [Experimental("SKEXP0010")]
    public static IServiceCollection AddLLM(this IServiceCollection services, IConfiguration configuration, Action<IKernelBuilder>? configureKernel = null)
    {
        services.Configure<LlmOptions>(configuration.GetSection("Config:Llm"));

        services.AddSingleton(sp =>
                              {
                                  var llmOptions    = sp.GetRequiredService<IOptions<LlmOptions>>().Value;
                                  var kernelBuilder = Kernel.CreateBuilder();

                                  kernelBuilder.AddOpenAIChatCompletion(llmOptions.TextModel, new Uri(llmOptions.TextEndpoint), llmOptions.TextApiKey, serviceId: "text");
                                  kernelBuilder.AddOpenAIChatCompletion(llmOptions.VisionEndpoint, new Uri(llmOptions.VisionEndpoint), llmOptions.VisionApiKey, serviceId: "vision");

                                  configureKernel?.Invoke(kernelBuilder);

                                  return kernelBuilder.Build();
                              });

        services.AddSingleton<ITextFilter, NameTextFilter>();

        return services;
    }

    public static IServiceCollection AddTTSSystem(
        this IServiceCollection services,
        IConfiguration          configuration)
    {
        // Add configuration
        services.Configure<TtsConfiguration>(configuration.GetSection("Config:Tts"));
        services.Configure<KokoroVoiceOptions>(configuration.GetSection("Config:Tts:Voice"));

        // Add core TTS components
        services.AddSingleton<ITtsEngine, TtsEngine>();

        // Add text processing components
        services.AddSingleton<ITextProcessor, TextProcessor>();
        services.AddSingleton<ITextNormalizer, TextNormalizer>();
        services.AddSingleton<ISentenceSegmenter, SentenceSegmenter>();
        services.AddSingleton<IMlSentenceDetector>(provider =>
                                                   {
                                                       var logger        = provider.GetRequiredService<ILogger<OpenNlpSentenceDetector>>();
                                                       var modelProvider = provider.GetRequiredService<IModelProvider>();
                                                       var basePath      = modelProvider.GetModelAsync(TTS.Synthesis.ModelType.OpenNLPDir).GetAwaiter().GetResult().Path;
                                                       var modelPath     = Path.Combine(basePath, "EnglishSD.nbin");

                                                       return new OpenNlpSentenceDetector(modelPath, logger);
                                                   });

        // Add phoneme processing components
        services.AddSingleton<IPhonemizer>(provider =>
                                           {
                                               var posTagger = provider.GetRequiredService<IPosTagger>();
                                               var lexicon   = provider.GetRequiredService<ILexicon>();
                                               var fallback  = provider.GetRequiredService<IFallbackPhonemizer>();

                                               return new PhonemizerG2P(posTagger, lexicon, fallback);
                                           });

        services.AddSingleton<IPosTagger>(provider =>
                                          {
                                              var logger        = provider.GetRequiredService<ILogger<OpenNlpPosTagger>>();
                                              var modelProvider = provider.GetRequiredService<IModelProvider>();

                                              var basePath  = modelProvider.GetModelAsync(TTS.Synthesis.ModelType.OpenNLPDir).GetAwaiter().GetResult().Path;
                                              var modelPath = Path.Combine(basePath, "EnglishPOS.nbin");

                                              return new OpenNlpPosTagger(modelPath, logger);
                                          });

        services.AddSingleton<ILexicon, Lexicon>();
        services.AddSingleton<IFallbackPhonemizer, EspeakFallbackPhonemizer>();

        services.AddSingleton<IAudioSynthesizer, OnnxAudioSynthesizer>();
        services.AddSingleton<IModelProvider>(provider =>
                                              {
                                                  var config = provider.GetRequiredService<IOptions<TtsConfiguration>>().Value;
                                                  var logger = provider.GetRequiredService<ILogger<FileModelProvider>>();

                                                  return new FileModelProvider(config.ModelDirectory, logger);
                                              });

        services.AddSingleton<IKokoroVoiceProvider, KokoroVoiceProvider>();
        services.AddSingleton<ITtsCache, TtsMemoryCache>();
        services.AddSingleton<IAudioFilter, BlacklistAudioFilter>();

        return services;
    }

    public static IServiceCollection AddUI(
        this IServiceCollection services,
        IConfiguration          configuration)
    {
        services.Configure<SubtitleOptions>(configuration.GetSection("Config:Subtitle"));
        services.Configure<RouletteWheelOptions>(configuration.GetSection("Config:RouletteWheel"));

        services.AddSingleton<IRenderComponent, SubtitleRenderer>();
        services.AddSingleton<RouletteWheel>();
        services.AddSingleton<IRenderComponent>(x => x.GetRequiredService<RouletteWheel>());
        services.AddConfigEditor();

        services.AddSingleton<FontProvider>();
        services.AddSingleton<IStartupTask>(x => x.GetRequiredService<FontProvider>());

        return services;
    }

    public static IServiceCollection AddLive2D(
        this IServiceCollection services,
        IConfiguration          configuration)
    {
        services.Configure<Live2DOptions>(configuration.GetSection("Config:Live2D"));

        services.AddSingleton<IRenderComponent, Live2DManager>();
        services.AddSingleton<ILive2DAnimationService, VBridgerLipSyncService>();
        services.AddSingleton<ILive2DAnimationService, IdleBlinkingAnimationService>();
        services.AddEmotionProcessing(configuration);

        return services;
    }

    public static IServiceCollection AddConfigEditor(this IServiceCollection services)
    {
        services.AddSingleton<IUiConfigurationManager, UiConfigurationManager>();
        services.AddSingleton<IEditorStateManager, EditorStateManager>();
        services.AddSingleton<IConfigSectionRegistry, ConfigSectionRegistry>();
        services.AddSingleton<IUiThemeManager, UiThemeManager>();
        services.AddSingleton<INotificationService, NotificationService>();

        services.AddSingleton<IRenderComponent, ConfigEditorComponent>();

        services.AddSingleton<TtsConfigEditor>();
        services.AddSingleton<RouletteWheelEditor>();
        services.AddSingleton<ChatEditor>();
        services.AddSingleton<MicrophoneConfigEditor>();

        services.AddSingleton<IStartupTask, ConfigSectionRegistrationTask>();

        return services;
    }

    public static IServiceCollection AddRVC(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<RVCFilterOptions>(configuration.GetSection("Config:Tts:Rvc"));

        services.AddSingleton<IRVCVoiceProvider, RVCVoiceProvider>();
        services.AddSingleton<IAudioFilter, RVCFilter>();

        return services;
    }

    public static IServiceCollection AddEmotionProcessing(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSingleton<IEmotionService, EmotionService>();
        services.AddSingleton<ITextFilter, EmotionProcessor>();
        services.AddSingleton<IAudioFilter, EmotionAudioFilter>();
        services.AddSingleton<ILive2DAnimationService, EmotionAnimationService>();

        return services;
    }

    public static IServiceCollection AddPolly(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddResiliencePipeline("semantickernel-chat", pipelineBuilder =>
                                                              {
                                                                  pipelineBuilder
                                                                      .AddTimeout(TimeSpan.FromSeconds(60))
                                                                      .AddRetry(new RetryStrategyOptions {
                                                                                Name = "ChatServiceRetry",
                                                                                ShouldHandle = new PredicateBuilder().Handle<Exception>(ex => ex is HttpRequestException ||
                                                                                ex is TimeoutRejectedException ||
                                                                                (ex is KernelException ke && ke.Message.Contains("transient", StringComparison.OrdinalIgnoreCase))),
                                                                                Delay            = TimeSpan.FromSeconds(2),
                                                                                BackoffType      = DelayBackoffType.Exponential,
                                                                                MaxDelay         = TimeSpan.FromSeconds(30),
                                                                                MaxRetryAttempts = 3,
                                                                                UseJitter        = true,
                                                                                OnRetry = args =>
                                                                                {
                                                                                var sessionId = args.Context.Properties.TryGetValue(ResilienceKeys.SessionId, out var x) ? x : Guid.Empty;
                                                                                var logger    = args.Context.Properties.TryGetValue(ResilienceKeys.Logger, out var y) ? y : NullLogger.Instance;
                                                                                logger.LogWarning("Request failed/stopped for session {SessionId}. Retrying in {Timespan}. Attempt {RetryAttempt}...",
                                                                                sessionId, args.Duration, args.AttemptNumber);

                                                                                return ValueTask.CompletedTask;
                                                                                }
                                                                                });
                                                              });

        return services;
    }
}

// SubtitleRenderer.cs

using System.Collections.Concurrent;
using System.Drawing;

using FontStashSharp;

using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.TTS.Synthesis;
using PersonaEngine.Lib.UI.Text.Rendering;

using Silk.NET.Input;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Main class responsible for rendering subtitles using the revised system.
///     Integrates with audio events, processes segments, manages the timeline, and draws text.
/// </summary>
public class SubtitleRenderer : IRenderComponent
{
    private readonly IAudioProgressNotifier _audioNotifier;

    private readonly SubtitleOptions _config;

    private readonly FontProvider _fontProvider;

    private readonly FSColor _highlightColor;

    private readonly FSColor _normalColor;

    private readonly ConcurrentQueue<AudioSegment> _pendingSegments = new();

    private TimeSpan _currentPlaybackTime = TimeSpan.Zero;

    private GL? _gl;

    private bool _isDisposed = false;

    private List<SubtitleLine> _linesToRender = new();

    private Task _processingTask = Task.CompletedTask;

    private SubtitleProcessor _subtitleProcessor;

    private SubtitleTimeline _subtitleTimeline;

    private TextMeasurer _textMeasurer;

    private TextRenderer _textRenderer;

    private int _viewportHeight;

    private int _viewportWidth;

    private IWordAnimator _wordAnimator;

    public SubtitleRenderer(
        IOptions<SubtitleOptions> configOptions,
        IAudioProgressNotifier    audioNotifier,
        FontProvider              fontProvider)
    {
        _config        = configOptions.Value;
        _audioNotifier = audioNotifier;
        _fontProvider  = fontProvider;

        var normalColorSys = ColorTranslator.FromHtml(_config.Color);
        _normalColor = new FSColor(normalColorSys.R, normalColorSys.G, normalColorSys.B, normalColorSys.A);
        var hColorSys = ColorTranslator.FromHtml(_config.HighlightColor);
        _highlightColor = new FSColor(hColorSys.R, hColorSys.G, hColorSys.B, hColorSys.A);

        SubscribeToAudioNotifier();
    }

    public void Dispose()
    {
        if ( _isDisposed )
        {
            return;
        }

        _isDisposed = true;

        UnsubscribeFromAudioNotifier();

        _pendingSegments.Clear();

        _linesToRender.Clear();

        GC.SuppressFinalize(this);
    }

    public void Initialize(GL gl, IView view, IInputContext input)
    {
        _gl             = gl;
        _viewportWidth  = view.Size.X;
        _viewportHeight = view.Size.Y;

        var fontSystem = _fontProvider.GetFontSystem(_config.Font);
        var font       = fontSystem.GetFont(_config.FontSize);

        _textMeasurer      = new TextMeasurer(font, _config.SideMargin, _viewportWidth, _viewportHeight);
        _subtitleProcessor = new SubtitleProcessor(_textMeasurer, _config.AnimationDuration);
        _wordAnimator      = new PopAnimator();

        _subtitleTimeline = new SubtitleTimeline(
                                                 _config.MaxVisibleLines,
                                                 _config.BottomMargin,
                                                 _textMeasurer.LineHeight + 0.5f,
                                                 _config.InterSegmentSpacing,
                                                 _textMeasurer,
                                                 _wordAnimator,
                                                 _highlightColor,
                                                 _normalColor
                                                );

        _textRenderer = new TextRenderer(_gl);

        Resize();
    }

    public bool UseSpout { get; } = true;

    public string SpoutTarget { get; } = "Live2D";

    public int Priority { get; } = -100;

    public void Update(float deltaTime)
    {
        if ( _isDisposed )
        {
            return;
        }

        var currentTime = (float)_currentPlaybackTime.TotalSeconds;

        _subtitleTimeline.Update(currentTime);

        _linesToRender = _subtitleTimeline.GetVisibleLinesAndPosition(currentTime, _viewportWidth, _viewportHeight);
    }

    public void Render(float deltaTime)
    {
        if ( _isDisposed || _gl == null )
        {
            return;
        }

        _textRenderer.Begin();

        foreach ( var line in _linesToRender )
        {
            foreach ( var word in line.Words )
            {
                if ( word.AnimationProgress > 0 || word.IsActive((float)_currentPlaybackTime.TotalSeconds) )
                {
                    _textMeasurer.Font.DrawText(
                                                _textRenderer,
                                                word.Text,
                                                word.Position,
                                                word.CurrentColor,
                                                scale: word.CurrentScale,
                                                origin: word.Size / 2.0f,
                                                effect: FontSystemEffect.Stroked,
                                                effectAmount: _config.StrokeThickness
                                               );
                }
            }
        }

        _textRenderer.End();
    }

    public void Resize()
    {
        _viewportWidth  = _config.Width;
        _viewportHeight = _config.Height;

        _textMeasurer.UpdateViewport(_viewportWidth, _viewportHeight);
        _textRenderer.OnViewportChanged(_viewportWidth, _viewportHeight);
    }

    private void SubscribeToAudioNotifier()
    {
        _audioNotifier.ChunkPlaybackStarted += OnChunkPlaybackStarted;
        _audioNotifier.ChunkPlaybackEnded   += OnChunkPlaybackEnded;
        _audioNotifier.PlaybackProgress     += OnPlaybackProgress;
    }

    private void UnsubscribeFromAudioNotifier()
    {
        _audioNotifier.ChunkPlaybackStarted -= OnChunkPlaybackStarted;
        _audioNotifier.ChunkPlaybackEnded   -= OnChunkPlaybackEnded;
        _audioNotifier.PlaybackProgress     -= OnPlaybackProgress;
    }

    private void OnChunkPlaybackStarted(object? sender, AudioChunkPlaybackStartedEvent e)
    {
        _pendingSegments.Enqueue(e.Chunk);

        if ( _processingTask.IsCompleted )
        {
            _processingTask = Task.Run(ProcessPendingSegmentsAsync);
        }
    }

    private void OnChunkPlaybackEnded(object? sender, AudioChunkPlaybackEndedEvent e) { _subtitleTimeline.RemoveSegment(e.Chunk); }

    private void OnPlaybackProgress(object? sender, AudioPlaybackProgressEvent e) { _currentPlaybackTime = e.CurrentPlaybackTime; }

    private void ProcessPendingSegmentsAsync()
    {
        while ( _pendingSegments.TryDequeue(out var audioSegment) )
        {
            try
            {
                var segmentStartTime = (float)0f;

                var processedSegment = _subtitleProcessor.ProcessSegment(audioSegment, segmentStartTime);

                _subtitleTimeline.AddSegment(processedSegment);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error processing subtitle segment: {ex.Message}");
            }
        }
    }
}

using System.Diagnostics;
using System.Threading.Channels;

using Microsoft.Extensions.Logging;

using OpenAI.Chat;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;
using PersonaEngine.Lib.Core.Conversation.Implementations.Context;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Input;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.Core.Conversation.Implementations.Metrics;
using PersonaEngine.Lib.LLM;
using PersonaEngine.Lib.TTS.Synthesis;

using Stateless;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Session;

public partial class ConversationSession : IConversationSession
{
    private static readonly ParticipantInfo ASSISTANT_PARTICIPANT = new("ARIA_ASSISTANT_BOT", "Aria", ChatMessageRole.Assistant);

    public ConversationSession(ILogger logger, IChatEngine chatEngine, ITtsEngine ttsEngine, IEnumerable<IInputAdapter> inputAdapters, IOutputAdapter outputAdapter, ConversationMetrics metrics, Guid sessionId, ConversationOptions options, ConversationContext context, IBargeInStrategy bargeInStrategy)
    {
        _logger     = logger;
        _chatEngine = chatEngine;
        _ttsEngine  = ttsEngine;

        _inputAdapters.AddRange(inputAdapters);
        _outputAdapter   = outputAdapter;
        _metrics         = metrics;
        SessionId        = sessionId;
        _options         = options;
        _context         = context;
        _bargeInStrategy = bargeInStrategy;

        _inputChannel       = Channel.CreateUnbounded<IInputEvent>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
        _inputChannelWriter = _inputChannel.Writer;

        _outputChannel       = Channel.CreateUnbounded<IOutputEvent>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
        _outputChannelWriter = _outputChannel.Writer;

        _stateMachine = new StateMachine<ConversationState, ConversationTrigger>(ConversationState.Initial);
        ConfigureStateMachine();
    }

    public async ValueTask DisposeAsync()
    {
        if ( _isDisposed )
        {
            return;
        }

        _logger.LogDebug("{SessionId} | Disposing session.", SessionId);

        if ( !_sessionCts.IsCancellationRequested )
        {
            await _sessionCts.CancelAsync();
        }

        if ( _sessionLoopTask is { IsCompleted: false } )
        {
            try
            {
                await _sessionLoopTask.WaitAsync(TimeSpan.FromSeconds(5));
            }
            catch (TimeoutException)
            {
                _logger.LogWarning("{SessionId} | Timeout waiting for session loop task during dispose.", SessionId);
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                _logger.LogError(ex, "{SessionId} | Error waiting for session loop task during dispose.", SessionId);
            }
        }

        await CleanupSessionAsync();

        _currentTurnCts?.Dispose();
        _sessionCts.Dispose();

        _inputChannelWriter.TryComplete();
        _outputChannelWriter.TryComplete();

        _isDisposed = true;

        GC.SuppressFinalize(this);
        _logger.LogInformation("{SessionId} | Session disposed.", SessionId);
    }

    public IConversationContext Context => _context;

    public Guid SessionId { get; }

    public async ValueTask RunAsync(CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_isDisposed, this);

        if ( _sessionLoopTask is { IsCompleted: false } )
        {
            _logger.LogWarning("{SessionId} | RunAsync called while session is already running.", SessionId);
            await _sessionLoopTask;

            return;
        }

        using var linkedCts   = CancellationTokenSource.CreateLinkedTokenSource(_sessionCts.Token, cancellationToken);
        var       linkedToken = linkedCts.Token;

        var inputLoopTask  = ProcessInputEventsAsync(linkedToken);
        var outputLoopTask = ProcessOutputEventsAsync(linkedToken);

        _sessionLoopTask = Task.WhenAll(inputLoopTask, outputLoopTask);

        try
        {
            await _stateMachine.FireAsync(ConversationTrigger.InitializeRequested);
            await _sessionLoopTask;

            _logger.LogInformation("{SessionId} | Session run completed. Final State: {State}", SessionId, _stateMachine.State);
        }
        catch (OperationCanceledException) when (linkedToken.IsCancellationRequested)
        {
            _logger.LogInformation("{SessionId} | RunAsync cancelled externally or by StopAsync.", SessionId);

            await EnsureStateMachineStoppedAsync();
        }
        catch (Exception ex)
        {
            _logger.LogCritical(ex, "{SessionId} | Unhandled exception during RunAsync setup or state machine trigger.", SessionId);
            await FireErrorAsync(ex);
        }
        finally
        {
            _logger.LogDebug("{SessionId} | RunAsync finished execution.", SessionId);
            await CleanupSessionAsync();

            _inputChannelWriter.TryComplete();
            _outputChannelWriter.TryComplete();
            _sessionLoopTask = null;
        }
    }

    public async ValueTask StopAsync()
    {
        _logger.LogDebug("{SessionId} | StopAsync called. Current State: {State}", SessionId, _stateMachine.State);

        if ( !_sessionCts.IsCancellationRequested )
        {
            await _sessionCts.CancelAsync();
        }
    }

    #region Event Processing Loops

    private async Task ProcessInputEventsAsync(CancellationToken cancellationToken)
    {
        _logger.LogDebug("{SessionId} | Starting input event processing loop.", SessionId);

        try
        {
            await foreach ( var inputEvent in _inputChannel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false) )
            {
                if ( cancellationToken.IsCancellationRequested )
                {
                    break;
                }

                try
                {
                    if ( inputEvent is SttSegmentRecognizing && !_sttStartTime.HasValue )
                    {
                        _sttStartTime = inputEvent.Timestamp;
                    }

                    if ( inputEvent is SttSegmentRecognized sttRecognized && _sttStartTime.HasValue )
                    {
                        var sttDuration = (sttRecognized.Timestamp - _sttStartTime.Value).TotalMilliseconds;
                        _metrics.RecordSttSegmentDuration(sttDuration, SessionId, sttRecognized.ParticipantId);
                        _sttStartTime = null;
                    }

                    var (trigger, eventArgs) = MapInputEventToTrigger(inputEvent);

                    if ( trigger.HasValue )
                    {
                        _logger.LogTrace("{SessionId} | Input Loop: Firing trigger {Trigger} for event {EventType}", SessionId, trigger.Value, inputEvent.GetType().Name);

                        await _stateMachine.FireAsync(trigger.Value, eventArgs);
                    }
                    else
                    {
                        _logger.LogWarning("{SessionId} | Input Loop: Received unhandled input event type: {EventType}", SessionId, inputEvent.GetType().Name);
                    }
                }
                catch (InvalidOperationException ex)
                {
                    _logger.LogWarning(ex, "{SessionId} | Input Loop: State machine transition denied in state {State}. Trigger: {Trigger}, Event: {@Event}",
                                       SessionId, _stateMachine.State, MapInputEventToTrigger(inputEvent).Trigger, inputEvent);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "{SessionId} | Input Loop: Error processing input event: {EventType}. Firing ErrorOccurred trigger.", SessionId, inputEvent.GetType().Name);
                    // await FireErrorWithoutStoppingAsync(ex);
                    if ( _stateMachine.State is ConversationState.Error or ConversationState.Ended )
                    {
                        break;
                    }
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogDebug("{SessionId} | Input event processing loop cancelled.", SessionId);
        }
        catch (ChannelClosedException)
        {
            _logger.LogInformation("{SessionId} | Input channel closed, ending event processing loop.", SessionId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "{SessionId} | Unhandled exception in input event processing loop.", SessionId);
            await FireErrorAsync(ex);
        }
        finally
        {
            _logger.LogInformation("{SessionId} | Input event processing loop finished. Final State: {State}", SessionId, _stateMachine.State);
            _inputChannelWriter.TryComplete();
        }
    }

    private async Task ProcessOutputEventsAsync(CancellationToken cancellationToken)
    {
        _logger.LogDebug("{SessionId} | Starting output event processing loop.", SessionId);

        try
        {
            await foreach ( var outputEvent in _outputChannel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false) )
            {
                if ( cancellationToken.IsCancellationRequested )
                {
                    break;
                }

                try
                {
                    switch ( outputEvent )
                    {
                        case LlmStreamStartEvent:
                            _llmStopwatch = Stopwatch.StartNew();

                            break;
                        case LlmStreamEndEvent ev:
                            if ( _llmStopwatch != null )
                            {
                                _llmStopwatch.Stop();
                                _llmFinishReason = ev.FinishReason;
                                _metrics.RecordLlmDuration(_llmStopwatch.Elapsed.TotalMilliseconds, SessionId, ev.TurnId ?? Guid.Empty, _llmFinishReason);
                                _llmStopwatch = null;
                            }

                            break;
                        case TtsStreamStartEvent:
                            _ttsStopwatch ??= Stopwatch.StartNew();

                            break;
                        case TtsStreamEndEvent ev:
                            if ( _ttsStopwatch != null )
                            {
                                _ttsStopwatch.Stop();
                                _ttsFinishReason = ev.FinishReason;
                                _metrics.RecordTtsDuration(_ttsStopwatch.Elapsed.TotalMilliseconds, SessionId, ev.TurnId ?? Guid.Empty, _ttsFinishReason);
                                _ttsStopwatch = null;
                            }

                            break;
                        case AudioPlaybackStartedEvent ev:
                            if ( _firstAudioLatencyStopwatch != null && ev.TurnId.HasValue )
                            {
                                _firstAudioLatencyStopwatch.Stop();
                                _currentTurnFirstAudioLatencyMs = _firstAudioLatencyStopwatch.Elapsed.TotalMilliseconds;
                                _metrics.RecordFirstAudioLatency(_firstAudioLatencyStopwatch.Elapsed.TotalMilliseconds, SessionId, ev.TurnId.Value);
                                _firstAudioLatencyStopwatch = null;
                            }

                            _audioPlaybackStopwatch = Stopwatch.StartNew();

                            break;
                        case AudioPlaybackEndedEvent ev:
                            if ( _audioPlaybackStopwatch != null )
                            {
                                var apt = _audioPlaybackStopwatch;
                                apt.Stop();
                                _audioFinishReason = ev.FinishReason;
                                _metrics.RecordAudioPlaybackDuration(apt.Elapsed.TotalMilliseconds, SessionId, ev.TurnId ?? Guid.Empty, _audioFinishReason);
                                _audioPlaybackStopwatch = null;

                                if ( _turnStopwatch != null && ev.TurnId.HasValue )
                                {
                                    apt = _turnStopwatch;
                                    apt.Stop();

                                    var overallReason = _audioFinishReason;
                                    if ( _ttsFinishReason == CompletionReason.Error || _llmFinishReason == CompletionReason.Error )
                                    {
                                        overallReason = CompletionReason.Error;
                                    }
                                    else if ( _ttsFinishReason == CompletionReason.Cancelled || _llmFinishReason == CompletionReason.Cancelled )
                                    {
                                        overallReason = CompletionReason.Cancelled;
                                    }

                                    _metrics.RecordTurnDuration(apt.Elapsed.TotalMilliseconds, SessionId, ev.TurnId.Value, overallReason);
                                }
                            }

                            break;
                        case ErrorOutputEvent ev:
                            _metrics.IncrementErrors(SessionId, ev.TurnId, ev.Exception);

                            break;
                    }

                    var (trigger, eventArgs) = MapOutputEventToTrigger(outputEvent);

                    if ( trigger.HasValue )
                    {
                        _logger.LogTrace("{SessionId} | Output Loop: Firing trigger {Trigger} for event {EventType}", SessionId, trigger.Value, outputEvent.GetType().Name);

                        await _stateMachine.FireAsync(trigger.Value, eventArgs);
                    }
                }
                catch (InvalidOperationException ex)
                {
                    _logger.LogWarning(ex, "{SessionId} | Output Loop: State machine transition denied in state {State}. Trigger: {Trigger}, Event: {@Event}",
                                       SessionId, _stateMachine.State, MapOutputEventToTrigger(outputEvent).Trigger, outputEvent);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "{SessionId} | Output Loop: Error processing output event: {EventType}. Firing ErrorOccurred trigger.", SessionId, outputEvent.GetType().Name);
                    if ( _stateMachine.State is ConversationState.Error or ConversationState.Ended )
                    {
                        break;
                    }
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogDebug("{SessionId} | Output event processing loop cancelled.", SessionId);
        }
        catch (ChannelClosedException)
        {
            _logger.LogDebug("{SessionId} | Output channel closed, ending output event processing loop.", SessionId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "{SessionId} | Unhandled exception in output event processing loop.", SessionId);
            await FireErrorAsync(ex);
        }
        finally
        {
            _logger.LogInformation("{SessionId} | Output event processing loop finished. Final State: {State}", SessionId, _stateMachine.State);
            _inputChannelWriter.TryComplete();
        }
    }

    private (ConversationTrigger? Trigger, object? EventArgs) MapInputEventToTrigger(IInputEvent inputEvent)
    {
        return inputEvent switch {
            SttSegmentRecognizing ev => (ConversationTrigger.InputDetected, ev),
            SttSegmentRecognized ev => (ConversationTrigger.InputFinalized, ev),
            _ => (null, null)
        };
    }

    private (ConversationTrigger? Trigger, object? EventArgs) MapOutputEventToTrigger(IOutputEvent outputEvent)
    {
        return outputEvent switch {
            LlmStreamStartEvent ev => (ConversationTrigger.LlmStreamStarted, ev),
            LlmChunkEvent ev => (ConversationTrigger.LlmStreamChunkReceived, ev),
            LlmStreamEndEvent ev => (ConversationTrigger.LlmStreamEnded, ev),
            TtsStreamStartEvent ev => (ConversationTrigger.TtsStreamStarted, ev),
            TtsChunkEvent ev => (ConversationTrigger.TtsStreamChunkReceived, ev),
            TtsStreamEndEvent ev => (ConversationTrigger.TtsStreamEnded, ev),
            AudioPlaybackStartedEvent ev => (ConversationTrigger.AudioStreamStarted, ev),
            AudioPlaybackEndedEvent ev => (ConversationTrigger.AudioStreamEnded, ev),
            ErrorOutputEvent ev => (ConversationTrigger.ErrorOccurred, ev.Exception),
            _ => (null, null)
        };
    }

    #endregion

    #region Fields

    private readonly IBargeInStrategy _bargeInStrategy;

    private Task? _llmTask;

    private Task? _ttsTask;

    private Task? _audioTask;

    private readonly ILogger _logger;

    private readonly ConversationOptions _options;

    private readonly IChatEngine _chatEngine;

    private readonly ITtsEngine _ttsEngine;

    private readonly List<IInputAdapter> _inputAdapters = new();

    private readonly IOutputAdapter _outputAdapter;

    private readonly CancellationTokenSource _sessionCts = new();

    private CancellationTokenSource? _currentTurnCts;

    private Task? _sessionLoopTask;

    private Guid? _currentTurnId;

    private readonly Channel<IInputEvent> _inputChannel;

    private readonly ChannelWriter<IInputEvent> _inputChannelWriter;

    private readonly Channel<IOutputEvent> _outputChannel;

    private readonly ChannelWriter<IOutputEvent> _outputChannelWriter;

    private Channel<LlmChunkEvent>? _currentLlmChannel;

    private Channel<TtsChunkEvent>? _currentTtsChannel;

    private bool _isDisposed = false;

    private readonly ConversationContext _context;

    private CompletionReason _llmFinishReason = CompletionReason.Completed;

    private CompletionReason _ttsFinishReason = CompletionReason.Completed;

    private CompletionReason _audioFinishReason = CompletionReason.Completed;

    private Stopwatch? _turnStopwatch;

    private Stopwatch? _llmStopwatch;

    private Stopwatch? _ttsStopwatch;

    private Stopwatch? _audioPlaybackStopwatch;

    private DateTimeOffset? _sttStartTime;

    private readonly ConversationMetrics _metrics;

    private Stopwatch? _firstAudioLatencyStopwatch;

    private double? _currentTurnFirstAudioLatencyMs;

    private Stopwatch? _firstLlmTokenLatencyStopwatch;

    private double? _currentTurnFirstLlmTokenLatencyMs;

    private Stopwatch? _firstTtsChunkLatencyStopwatch;

    private double? _currentTurnFirstTtsChunkLatencyMs;

    #endregion

    # region State Actions

    private async Task InitializeSessionAsync()
    {
        _logger.LogDebug("{SessionId} | Action: InitializeSessionAsync", SessionId);

        try
        {
            _context.TryAddParticipant(ASSISTANT_PARTICIPANT);

            var initInputTasks = _inputAdapters.Select(async adapter =>
                                                       {
                                                           await adapter.InitializeAsync(SessionId, _inputChannelWriter, _sessionCts.Token);
                                                           await adapter.StartAsync(_sessionCts.Token);
                                                           _context.TryAddParticipant(adapter.Participant);

                                                           _logger.LogDebug("{SessionId} | Input Adapter {AdapterId} initialized and started.", SessionId, adapter.AdapterId);
                                                       }).ToList();

            var initOutputTasks = Task.Run(async () =>
                                           {
                                               await _outputAdapter.InitializeAsync(SessionId, _sessionCts.Token);
                                               await _outputAdapter.StartAsync(_sessionCts.Token);
                                               _logger.LogDebug("{SessionId} | Output Adapter {AdapterId} initialized and started.", SessionId, _outputAdapter.AdapterId);
                                           });

            await Task.WhenAll(initInputTasks.Concat([initOutputTasks]));

            await _stateMachine.FireAsync(ConversationTrigger.InitializeComplete);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "{SessionId} | Error during InitializeSessionAsync.", SessionId);
            await _stateMachine.FireAsync(ConversationTrigger.ErrorOccurred, ex);
        }
    }

    private async Task PrepareLlmRequestAsync(IInputEvent inputEvent)
    {
        if ( inputEvent is not SttSegmentRecognized finalizedEvent )
        {
            _logger.LogWarning("{SessionId} | PrepareLlmRequestAsync received unexpected event type: {EventType}", SessionId, inputEvent.GetType().Name);
            await FireErrorAsync(new ArgumentException("Invalid event type for InputFinalized trigger.", nameof(inputEvent)), false);

            return;
        }

        _logger.LogDebug("{SessionId} | Action: PrepareLlmRequestAsync for input: \"{InputText}\"", SessionId, finalizedEvent.FinalTranscript);

        // In-case race condition (channel events not consumed fast enough)
        await CancelCurrentTurnProcessingAsync();

        _currentTurnCts = CancellationTokenSource.CreateLinkedTokenSource(_sessionCts.Token);
        _currentTurnId  = Guid.NewGuid();

        _turnStopwatch                 = Stopwatch.StartNew();
        _firstAudioLatencyStopwatch    = Stopwatch.StartNew(); // For first audio playback
        _firstLlmTokenLatencyStopwatch = Stopwatch.StartNew(); // For first LLM token
        _firstTtsChunkLatencyStopwatch = Stopwatch.StartNew(); // For first TTS chunk (if applicable)

        _metrics.IncrementTurnsStarted(SessionId);

        _llmFinishReason   = CompletionReason.Completed;
        _ttsFinishReason   = CompletionReason.Completed;
        _audioFinishReason = CompletionReason.Completed;

        try
        {
            _context.StartTurn(_currentTurnId.Value, [finalizedEvent.ParticipantId, ASSISTANT_PARTICIPANT.Id]);
            _context.AppendToTurn(finalizedEvent.ParticipantId, finalizedEvent.FinalTranscript);

            var userName = _context.Participants.TryGetValue(finalizedEvent.ParticipantId, out var pInfo) ? pInfo.Name : finalizedEvent.ParticipantId;
            _logger.LogInformation("{SessionId} | {TurnId} | 📝 | [{Speaker}]{Text}", SessionId, _currentTurnId, userName, _context.GetPendingMessageText(finalizedEvent.ParticipantId));

            _context.CompleteTurnPart(finalizedEvent.ParticipantId);

            await _stateMachine.FireAsync(ConversationTrigger.LlmRequestSent);

            if ( _outputAdapter is IAudioOutputAdapter )
            {
                await _stateMachine.FireAsync(ConversationTrigger.TtsRequestSent);
            }
            else
            {
                _firstTtsChunkLatencyStopwatch?.Stop();
                _firstTtsChunkLatencyStopwatch     = null;
                _currentTurnFirstTtsChunkLatencyMs = null;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "{SessionId} | Error during PrepareLlmRequestAsync for Turn {TurnId}.", SessionId, _currentTurnId ?? Guid.Empty);
            await FireErrorAsync(ex);
        }
    }

    private async Task CancelCurrentTurnProcessingAsync()
    {
        if ( _currentTurnCts == null )
        {
            _logger.LogDebug("{SessionId} | No active turn processing to cancel.", SessionId);

            return;
        }

        var turnIdBeingCancelled = _currentTurnId;
        _logger.LogDebug("{SessionId} | Requesting cancellation for Turn {TurnId}'s processing pipeline.", SessionId, turnIdBeingCancelled);

        if ( _currentTurnCts is { IsCancellationRequested: false } )
        {
            try
            {
                await _currentTurnCts.CancelAsync();
                _logger.LogTrace("{SessionId} | Turn {TurnId} CancellationToken cancelled.", SessionId, turnIdBeingCancelled);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{SessionId} | Error cancelling CancellationTokenSource for Turn {TurnId}.", SessionId, turnIdBeingCancelled);
            }
        }

        if ( turnIdBeingCancelled.HasValue )
        {
            await WaitForTaskCancellationAsync(_llmTask, "LLM response generation", turnIdBeingCancelled.Value);
            await WaitForTaskCancellationAsync(_ttsTask, "TTS generation", turnIdBeingCancelled.Value);
            await WaitForTaskCancellationAsync(_audioTask, "Audio processing", turnIdBeingCancelled.Value);
        }

        _currentTurnCts?.Dispose();
        _currentTurnCts = null;
        _currentTurnId  = null;
        _llmTask        = null;
        _ttsTask        = null;
        _audioTask      = null;

        _context.AbortTurn();

        _currentLlmChannel?.Writer.TryComplete(new OperationCanceledException());
        _currentTtsChannel?.Writer.TryComplete(new OperationCanceledException());

        _turnStopwatch?.Stop();
        _llmStopwatch?.Stop();
        _ttsStopwatch?.Stop();
        _audioPlaybackStopwatch?.Stop();
        _firstAudioLatencyStopwatch?.Stop();
        _firstLlmTokenLatencyStopwatch?.Stop();
        _firstTtsChunkLatencyStopwatch?.Stop();

        _turnStopwatch                  = null;
        _llmStopwatch                   = null;
        _ttsStopwatch                   = null;
        _audioPlaybackStopwatch         = null;
        _firstAudioLatencyStopwatch     = null;
        _firstLlmTokenLatencyStopwatch  = null;
        _firstTtsChunkLatencyStopwatch  = null;
        _currentTurnFirstAudioLatencyMs = null;
        _sttStartTime                   = null;

        _currentLlmChannel = null;
        _currentTtsChannel = null;
    }

    private async Task WaitForTaskCancellationAsync(Task? task, string taskName, Guid turnIdBeingCancelled)
    {
        if ( task is { IsCompleted: false } )
        {
            _logger.LogTrace("{SessionId} | Waiting briefly for {TaskName} task cancellation acknowledgment (Turn {TurnId}).", SessionId, taskName, turnIdBeingCancelled);
            try
            {
                await task;
            }
            catch (OperationCanceledException)
            {
                /* Ignored */
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{SessionId} | Exception while waiting for {TaskName} task cancellation (Turn {TurnId}). Task Status: {Status}", SessionId, taskName, turnIdBeingCancelled, task.Status);
            }
        }
        else
        {
            _logger.LogTrace("{SessionId} | {TaskName} task was null or already completed (Turn {TurnId}). No wait needed.", SessionId, taskName, turnIdBeingCancelled);
        }
    }

    private void HandleInterruption()
    {
        var interruptedTurnId = _currentTurnId;

        if ( !interruptedTurnId.HasValue )
        {
            _logger.LogWarning("{SessionId} | Interruption handled but no active turn ID found.", SessionId);

            return;
        }

        _metrics.IncrementTurnsInterrupted(SessionId, interruptedTurnId.Value);

        CommitChanges(false);
    }

    private async Task HandleInterruptionCancelAndRefireAsync(IInputEvent inputEvent, StateMachine<ConversationState, ConversationTrigger>.Transition transition)
    {
        HandleInterruption();

        if ( transition.Source is ConversationState.Speaking )
        {
            _context.CompleteTurnPart(ASSISTANT_PARTICIPANT.Id, true);
        }

        switch ( transition.Trigger )
        {
            case ConversationTrigger.InputDetected:
                await _stateMachine.FireAsync(_inputDetectedTrigger, inputEvent);

                return;
            case ConversationTrigger.InputFinalized:
                await _stateMachine.FireAsync(_inputFinalizedTrigger, inputEvent);

                return;
            default:
                throw new ArgumentException($"{transition} failed. The state machine is in an invalid state.", nameof(transition));
        }
    }

    private async Task PauseActivitiesAsync()
    {
        _logger.LogInformation("{SessionId} | Action: PauseActivitiesAsync - Stopping Adapters", SessionId);

        // TODO: Need to think about this
        foreach ( var adapter in _inputAdapters )
        {
            try
            {
                await adapter.StopAsync(_sessionCts.Token);
            } // Use session token? Or None?
            catch (Exception ex)
            {
                _logger.LogError(ex, "{SessionId} | Error stopping input adapter {AdapterId} during pause.", SessionId, adapter.AdapterId);
            }
        }
    }

    private async Task ResumeActivitiesAsync()
    {
        _logger.LogInformation("{SessionId} | Action: ResumeActivitiesAsync - Starting Adapters", SessionId);

        // TODO: Need to think about this
        foreach ( var adapter in _inputAdapters )
        {
            try
            {
                await adapter.StartAsync(_sessionCts.Token);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{SessionId} | Error starting input adapter {AdapterId} during resume.", SessionId, adapter.AdapterId);
            }
        }
    }

    private async Task HandleErrorAsync(Exception error)
    {
        _logger.LogError(error, "{SessionId} | Action: HandleErrorAsync - An error occurred in the state machine.", SessionId);
        _metrics.IncrementErrors(SessionId, _currentTurnId, error);
        await _stateMachine.FireAsync(ConversationTrigger.StopRequested);
    }

    private async Task CleanupSessionAsync()
    {
        await CancelCurrentTurnProcessingAsync();

        var inputCleanupTasks = _inputAdapters.Select(async adapter =>
                                                      {
                                                          try
                                                          {
                                                              await adapter.StopAsync(CancellationToken.None);
                                                              await adapter.DisposeAsync();
                                                              _logger.LogDebug("{SessionId} | Input Adapter {AdapterId} stopped and disposed.", SessionId, adapter.AdapterId);
                                                          }
                                                          catch (Exception ex)
                                                          {
                                                              _logger.LogError(ex, "{SessionId} | Error cleaning up input adapter {AdapterId}.", SessionId, adapter.AdapterId);
                                                          }
                                                      }).ToList();

        var outputCleanupTask = Task.Run(async () =>
                                         {
                                             try
                                             {
                                                 await _outputAdapter.StopAsync(CancellationToken.None);
                                                 await _outputAdapter.DisposeAsync();
                                                 _logger.LogDebug("{SessionId} | Output Adapter {AdapterId} stopped and disposed.", SessionId, _outputAdapter.AdapterId);
                                             }
                                             catch (Exception ex)
                                             {
                                                 _logger.LogError(ex, "{SessionId} | Error cleaning up output adapter {AdapterId}.", SessionId, _outputAdapter.AdapterId);
                                             }
                                         });

        await Task.WhenAll(inputCleanupTasks.Append(outputCleanupTask));
        _inputAdapters.Clear();
        _logger.LogDebug("{SessionId} | Cleanup finished.", SessionId);
    }

    private void HandleLlmStreamRequested()
    {
        var turnId       = _currentTurnId;
        var turnCts      = _currentTurnCts;
        var outputWriter = _outputChannelWriter;

        if ( !turnId.HasValue || turnCts is null )
        {
            _logger.LogError("{SessionId} | HandleLlmStreamRequested called in invalid state (TurnId: {TurnId}, Cts: {Cts}).",
                             SessionId, turnId, turnCts != null);

            _ = FireErrorAsync(new InvalidOperationException("Cannot start LLM stream in invalid state."), false);

            return;
        }

        _currentLlmChannel = Channel.CreateUnbounded<LlmChunkEvent>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });

        _llmTask = _chatEngine.GetStreamingChatResponseAsync(_context, outputWriter, turnId.Value, SessionId, cancellationToken: turnCts.Token);
    }

    private void HandleTtsStreamRequest()
    {
        var turnId              = _currentTurnId;
        var turnCts             = _currentTurnCts;
        var outputChannelWriter = _outputChannelWriter;
        var outputChannelReader = _currentLlmChannel;

        if ( !turnId.HasValue || turnCts is null || outputChannelReader is null )
        {
            _logger.LogWarning("{SessionId} | HandleTtsStreamRequest called without a valid TurnId.", SessionId);

            return;
        }

        _currentTtsChannel = Channel.CreateUnbounded<TtsChunkEvent>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });

        _ttsTask = _ttsEngine.SynthesizeStreamingAsync(outputChannelReader, outputChannelWriter, turnId.Value, SessionId, cancellationToken: turnCts.Token);

        if ( _outputAdapter is IAudioOutputAdapter audioOutput )
        {
            _audioTask = audioOutput.SendAsync(_currentTtsChannel, outputChannelWriter, turnId.Value, turnCts.Token);
        }
    }

    private async Task HandleLlmStreamChunkReceived(IOutputEvent outputEvent, StateMachine<ConversationState, ConversationTrigger>.Transition _)
    {
        if ( outputEvent is not LlmChunkEvent chunkEvent )
        {
            _logger.LogWarning("{SessionId} | HandleLlmStreamChunkReceived received unexpected event type: {EventType}", SessionId, outputEvent.GetType().Name);
            await _stateMachine.FireAsync(ConversationTrigger.ErrorOccurred, new ArgumentException("Invalid event type for LlmStreamChunkReceived trigger.", nameof(outputEvent)));

            return;
        }

        var turnId       = _currentTurnId;
        var turnCts      = _currentTurnCts;
        var outputWriter = _currentLlmChannel?.Writer;

        if ( !turnId.HasValue || turnCts is null || outputWriter is null )
        {
            _logger.LogWarning("{SessionId} | HandleLlmStreamChunkReceived called without a valid TurnId.", SessionId);

            return;
        }

        if ( _firstLlmTokenLatencyStopwatch != null )
        {
            _firstLlmTokenLatencyStopwatch.Stop();
            _currentTurnFirstLlmTokenLatencyMs = _firstLlmTokenLatencyStopwatch.Elapsed.TotalMilliseconds;
            _logger.LogDebug("{SessionId} | {TurnId} | ⏱ | First LLM [{LatencyMs:N0} ms]", SessionId, turnId.Value, _currentTurnFirstLlmTokenLatencyMs.Value);
            _firstLlmTokenLatencyStopwatch = null;
        }

        _context.AppendToTurn(ASSISTANT_PARTICIPANT.Id, chunkEvent.Chunk);

        await outputWriter.WriteAsync(chunkEvent, turnCts.Token);
    }

    private async Task HandleTtsStreamChunkReceived(IOutputEvent outputEvent, StateMachine<ConversationState, ConversationTrigger>.Transition _)
    {
        if ( outputEvent is not TtsChunkEvent chunkEvent )
        {
            _logger.LogWarning("{SessionId} | HandleTtsStreamChunkReceived received unexpected event type: {EventType}", SessionId, outputEvent.GetType().Name);
            await _stateMachine.FireAsync(ConversationTrigger.ErrorOccurred, new ArgumentException("Invalid event type for HandleTtsStreamChunkReceived trigger.", nameof(outputEvent)));

            return;
        }

        var turnId       = _currentTurnId;
        var turnCts      = _currentTurnCts;
        var outputWriter = _currentTtsChannel?.Writer;

        if ( !turnId.HasValue || turnCts is null || outputWriter is null )
        {
            _logger.LogWarning("{SessionId} | HandleTtsStreamChunkReceived called without a valid TurnId.", SessionId);

            return;
        }

        if ( _firstTtsChunkLatencyStopwatch != null )
        {
            _firstTtsChunkLatencyStopwatch.Stop();
            _currentTurnFirstTtsChunkLatencyMs = _firstTtsChunkLatencyStopwatch.Elapsed.TotalMilliseconds;
            _logger.LogDebug("{SessionId} | {TurnId} | ⏱ | First TTS [{LatencyMs:N0} ms]", SessionId, turnId.Value, _currentTurnFirstTtsChunkLatencyMs.Value);
            _firstTtsChunkLatencyStopwatch = null;
        }

        await outputWriter.WriteAsync(chunkEvent, turnCts.Token);
    }

    private void HandleLlmStreamEnded()
    {
        var outputWriter = _currentLlmChannel?.Writer;

        if ( outputWriter is null )
        {
            _logger.LogWarning("{SessionId} | HandleLlmStreamEnded called without a valid LLM channel.", SessionId);

            return;
        }

        outputWriter.TryComplete();

        if ( _outputAdapter is not IAudioOutputAdapter )
        {
            _context.CompleteTurnPart(ASSISTANT_PARTICIPANT.Id);
        }
    }

    private void HandleTtsStreamEnded()
    {
        var outputWriter = _currentTtsChannel?.Writer;

        if ( outputWriter is null )
        {
            _logger.LogWarning("{SessionId} | HandleTtsStreamEnded called without a valid TTS channel.", SessionId);

            return;
        }

        outputWriter.TryComplete();
    }

    private async Task FireErrorAsync(Exception ex, bool stopSession = true)
    {
        try
        {
            if ( _stateMachine.State != ConversationState.Error && _stateMachine.State != ConversationState.Ended )
            {
                await _stateMachine.FireAsync(ConversationTrigger.ErrorOccurred, ex);
            }

            if ( stopSession && _stateMachine.State != ConversationState.Ended )
            {
                await StopAsync();
            }
        }
        catch (Exception fireEx)
        {
            _logger.LogError(fireEx, "{SessionId} | Failed to fire ErrorOccurred/StopRequested after initial exception.", SessionId);
            if ( !_sessionCts.IsCancellationRequested )
            {
                await _sessionCts.CancelAsync();
            }
        }
    }

    private async Task EnsureStateMachineStoppedAsync()
    {
        if ( _stateMachine.State != ConversationState.Ended && _stateMachine.State != ConversationState.Error )
        {
            _logger.LogWarning("{SessionId} | Session loop ended unexpectedly in state {State}. Forcing StopRequested.", SessionId, _stateMachine.State);
            try
            {
                if ( !_sessionCts.IsCancellationRequested )
                {
                    await _sessionCts.CancelAsync();
                }

                await _stateMachine.FireAsync(ConversationTrigger.StopRequested);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "{SessionId} | Exception while trying to force StopRequested.", SessionId);
            }
        }
    }

    private void CommitSpokenText() { _context.CompleteTurnPart(ASSISTANT_PARTICIPANT.Id); }

    private void HandleIdle()
    {
        var completedTurnId = _currentTurnId;
        var turnDuration    = _turnStopwatch?.Elapsed.TotalMilliseconds;

        if ( completedTurnId.HasValue && turnDuration.HasValue )
        {
            var llmTime   = _currentTurnFirstLlmTokenLatencyMs ?? 0;
            var ttsTime   = _currentTurnFirstTtsChunkLatencyMs ?? 0;
            var audioTime = _currentTurnFirstAudioLatencyMs ?? 0;

            _logger.LogInformation(
                                   "{SessionId} | {TurnId} | ⏱ | LLM @{LlmMs:N0}ms ⇢ TTS @{TtsMs:N0}ms ⇢ AUDIO @{AudioMs:N0}ms | TOTAL @{TotalMs:N0}ms",
                                   SessionId,
                                   completedTurnId.Value,
                                   llmTime,
                                   ttsTime - llmTime,
                                   audioTime - ttsTime,
                                   audioTime
                                  );
        }

        CommitChanges(false);

        _currentTurnId                 = null;
        _turnStopwatch                 = null;
        _llmStopwatch                  = null;
        _ttsStopwatch                  = null;
        _audioPlaybackStopwatch        = null;
        _firstAudioLatencyStopwatch    = null;
        _firstLlmTokenLatencyStopwatch = null;
        _firstTtsChunkLatencyStopwatch = null;

        _currentTurnFirstAudioLatencyMs    = null;
        _currentTurnFirstLlmTokenLatencyMs = null;
        _currentTurnFirstTtsChunkLatencyMs = null;
        _sttStartTime                      = null;

        _currentLlmChannel?.Writer.TryComplete();
        _currentTtsChannel?.Writer.TryComplete();
        _currentLlmChannel = null;
        _currentTtsChannel = null;
    }

    private void CommitChanges(bool interrupted)
    {
        foreach ( var inputAdapter in _inputAdapters )
        {
            _context.CompleteTurnPart(inputAdapter.Participant.Id, interrupted);
        }
    }

    #endregion
}

using System.Collections.Concurrent;

using Microsoft.Extensions.Logging;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Voice provider implementation to manage voice embeddings
/// </summary>
public class KokoroVoiceProvider : IKokoroVoiceProvider
{
    private readonly ITtsCache _cache;

    private readonly ILogger<KokoroVoiceProvider> _logger;

    private readonly IModelProvider _modelProvider;

    private readonly ConcurrentDictionary<string, string> _voicePaths = new();

    private bool _disposed;

    public KokoroVoiceProvider(
        IModelProvider               ittsModelProvider,
        ITtsCache                    cache,
        ILogger<KokoroVoiceProvider> logger)
    {
        _modelProvider = ittsModelProvider ?? throw new ArgumentNullException(nameof(ittsModelProvider));
        _cache         = cache ?? throw new ArgumentNullException(nameof(cache));
        _logger        = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<VoiceData> GetVoiceAsync(
        string            voiceId,
        CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(voiceId) )
        {
            throw new ArgumentException("Voice ID cannot be null or empty", nameof(voiceId));
        }

        try
        {
            // Use cache to avoid repeated loading
            return await _cache.GetOrAddAsync($"voice_{voiceId}", async ct =>
                                                                  {
                                                                      _logger.LogDebug("Loading voice data for {VoiceId}", voiceId);

                                                                      // Get voice directory
                                                                      var voiceDirPath = await GetVoicePathAsync(voiceId, ct);

                                                                      // Load binary data
                                                                      var bytes = await File.ReadAllBytesAsync(voiceDirPath, ct);

                                                                      // Convert to float array
                                                                      var embedding = new float[bytes.Length / sizeof(float)];
                                                                      Buffer.BlockCopy(bytes, 0, embedding, 0, bytes.Length);

                                                                      _logger.LogInformation("Loaded voice {VoiceId} with {EmbeddingSize} embedding dimensions",
                                                                                voiceId, embedding.Length);

                                                                      return new VoiceData(voiceId, embedding);
                                                                  }, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error loading voice {VoiceId}", voiceId);

            throw;
        }
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<string>> GetAvailableVoicesAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            // Get voice directory
            var model     = await _modelProvider.GetModelAsync(ModelType.KokoroVoices, cancellationToken);
            var voicesDir = model.Path;

            if ( !Directory.Exists(voicesDir) )
            {
                _logger.LogWarning("Voices directory not found: {Path}", voicesDir);

                return Array.Empty<string>();
            }

            // Get all .bin files
            var voiceFiles = Directory.GetFiles(voicesDir, "*.bin");

            // Extract voice IDs from filenames
            var voiceIds = new List<string>(voiceFiles.Length);

            foreach ( var file in voiceFiles )
            {
                var voiceId = Path.GetFileNameWithoutExtension(file);
                voiceIds.Add(voiceId);

                // Cache the path for faster lookup
                _voicePaths[voiceId] = file;
            }

            _logger.LogInformation("Found {Count} available Kokoro voices", voiceIds.Count);

            return voiceIds;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting available voices");

            throw;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if ( _disposed )
        {
            return;
        }

        _voicePaths.Clear();
        _disposed = true;

        await Task.CompletedTask;
    }

    /// <summary>
    ///     Gets the file path for a voice
    /// </summary>
    private async Task<string> GetVoicePathAsync(string voiceId, CancellationToken cancellationToken)
    {
        // Check if path is cached
        if ( _voicePaths.TryGetValue(voiceId, out var cachedPath) )
        {
            return cachedPath;
        }

        // Get base directory
        var model     = await _modelProvider.GetModelAsync(ModelType.KokoroVoices, cancellationToken);
        var voicesDir = model.Path;

        // Build path
        var voicePath = Path.Combine(voicesDir, $"{voiceId}.bin");

        // Check if file exists
        if ( !File.Exists(voicePath) )
        {
            _logger.LogWarning("Voice file not found: {Path}", voicePath);

            throw new FileNotFoundException($"Voice file not found for {voiceId}", voicePath);
        }

        // Cache the path
        _voicePaths[voiceId] = voicePath;

        return voicePath;
    }
}

using System.Diagnostics;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace PersonaEngine.Lib.Audio.Player;

/// <summary>
///     Implementation of IPlaybackController that works with large audio segments.
/// </summary>
public class TimedPlaybackController : IPlaybackController
{
    private readonly int _initialBufferMs;

    private readonly double _latencyThresholdMs;

    private readonly ILogger _logger;

    private readonly Stopwatch _playbackTimer = new();

    // For time updates
    private readonly object _timeLock = new();

    private readonly int _timeUpdateIntervalMs;

    private readonly Timer _timeUpdateTimer;

    // Latency tracking

    private int _currentSampleRate;

    private double _lastReportedLatencyMs;

    private long _startTimeMs; // When playback actually started (system time)

    /// <summary>
    ///     Initializes a new instance of the TimedPlaybackController class.
    /// </summary>
    /// <param name="initialBufferMs">Initial buffer size in milliseconds.</param>
    /// <param name="latencyThresholdMs">Threshold for significant latency changes in milliseconds.</param>
    /// <param name="timeUpdateIntervalMs">Interval in milliseconds for time updates.</param>
    /// <param name="logger">Logger instance.</param>
    public TimedPlaybackController(
        int                               initialBufferMs      = 20,
        double                            latencyThresholdMs   = 20.0,
        int                               timeUpdateIntervalMs = 20,
        ILogger<TimedPlaybackController>? logger               = null)
    {
        _initialBufferMs      = initialBufferMs;
        _latencyThresholdMs   = latencyThresholdMs;
        _timeUpdateIntervalMs = timeUpdateIntervalMs;
        _logger               = logger ?? NullLogger<TimedPlaybackController>.Instance;

        // Start a timer to update the playback time periodically
        _timeUpdateTimer = new Timer(UpdateTime, null, Timeout.Infinite, Timeout.Infinite);
    }

    /// <inheritdoc />
    public float CurrentTime { get; private set; }

    /// <inheritdoc />
    public long TotalSamplesPlayed { get; private set; }

    /// <inheritdoc />
    public double CurrentLatencyMs { get; private set; }

    /// <inheritdoc />
    public bool HasLatencyInformation { get; private set; }

    /// <summary>
    ///     Event that fires when the playback time changes substantially.
    /// </summary>
    public event EventHandler<float> TimeChanged
    {
        add
        {
            lock (_timeLock)
            {
                TimeChangedInternal += value;
            }
        }
        remove
        {
            lock (_timeLock)
            {
                TimeChangedInternal -= value;
            }
        }
    }

    /// <inheritdoc />
    public event EventHandler<double>? LatencyChanged;

    /// <inheritdoc />
    public void Reset()
    {
        _playbackTimer.Reset();
        TotalSamplesPlayed = 0;
        CurrentTime        = 0;
        _startTimeMs       = 0;

        // Stop the time update timer
        _timeUpdateTimer.Change(Timeout.Infinite, Timeout.Infinite);
    }

    /// <inheritdoc />
    public void UpdateLatency(double latencyMs)
    {
        var oldLatency = CurrentLatencyMs;
        CurrentLatencyMs      = latencyMs;
        HasLatencyInformation = true;

        // Only notify if the change is significant
        if ( Math.Abs(latencyMs - _lastReportedLatencyMs) >= _latencyThresholdMs )
        {
            _logger.LogInformation("Playback latency updated: {OldLatency:F1}ms -> {NewLatency:F1}ms",
                                   oldLatency, latencyMs);

            _lastReportedLatencyMs = latencyMs;
            LatencyChanged?.Invoke(this, latencyMs);
        }
    }

    /// <inheritdoc />
    public Task<bool> SchedulePlaybackAsync(int samplesPerChannel, int sampleRate, CancellationToken cancellationToken)
    {
        // Store the sample rate for time calculations
        _currentSampleRate = sampleRate;

        // Update total samples
        TotalSamplesPlayed += samplesPerChannel;

        // Start the playback timer if this is the first call
        if ( !_playbackTimer.IsRunning )
        {
            _logger.LogDebug("Starting playback timer");

            // Account for initial buffering
            var initialBufferMs = HasLatencyInformation
                                      ? Math.Max(50, _initialBufferMs + CurrentLatencyMs)
                                      : _initialBufferMs;

            // Record when playback will actually start (now + buffer)
            _startTimeMs = Environment.TickCount64 + (long)initialBufferMs;

            // Start the stopwatch for measuring elapsed time
            _playbackTimer.Start();

            // Start the timer to update current time periodically
            _timeUpdateTimer.Change(_timeUpdateIntervalMs, _timeUpdateIntervalMs);
        }

        // Always send the data - the transport will handle chunking
        return Task.FromResult(true);
    }

    /// <summary>
    ///     Disposes resources used by the controller.
    /// </summary>
    public void Dispose() { _timeUpdateTimer.Dispose(); }

    private event EventHandler<float>? TimeChangedInternal;

    // Private methods

    private void UpdateTime(object? state)
    {
        if ( !_playbackTimer.IsRunning || _currentSampleRate <= 0 )
        {
            return;
        }

        // Calculate current system time
        var currentTimeMs = Environment.TickCount64;

        // Calculate how much time has passed since playback started
        var elapsedSinceStartMs = currentTimeMs > _startTimeMs
                                      ? currentTimeMs - _startTimeMs
                                      : 0;

        // Apply latency adjustment if available
        var adjustedElapsedMs = HasLatencyInformation
                                    ? elapsedSinceStartMs - CurrentLatencyMs
                                    : elapsedSinceStartMs;

        // Don't allow negative time
        adjustedElapsedMs = Math.Max(0, adjustedElapsedMs);

        // Convert to seconds
        var newPlaybackTime = (float)(adjustedElapsedMs / 1000.0);

        // Only update and notify if the time changed significantly
        if ( Math.Abs(newPlaybackTime - CurrentTime) >= 0.01f ) // 10ms threshold
        {
            CurrentTime = newPlaybackTime;

            EventHandler<float>? handler;
            lock (_timeLock)
            {
                handler = TimeChangedInternal;
            }

            handler?.Invoke(this, newPlaybackTime);
        }
    }
}

using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

using PersonaEngine.Lib.TTS.Profanity;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.TTS.Audio;

/// <summary>
///     Audio filter that censors profane content by replacing it with a beep tone.
/// </summary>
public class BlacklistAudioFilter : IAudioFilter
{
    // Configuration constants
    private const float DEFAULT_BEEP_FREQUENCY = 880; // Changed from 400Hz to 1000Hz (standard censor beep)

    private const float DEFAULT_BEEP_VOLUME = 0.35f; // Slightly increased for better audibility

    private const int REFERENCE_SAMPLE_RATE = 24000;

    // Common suffixes to check when matching profanity
    private static readonly string[] COMMON_SUFFIXES = { "s", "es", "ed", "ing", "er", "ers" };

    // Pre-calculated beep tone samples
    private readonly float[] _beepCycle;

    private readonly float _beepFrequency;

    private readonly float _beepVolume;

    private readonly ProfanityDetector _profanityDetector;

    private readonly HashSet<string> _profanityDictionary;

    // Cache for previously checked words to improve performance
    private readonly Dictionary<string, bool> _wordCheckCache = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>
    ///     Initializes a new instance of the <see cref="BlacklistAudioFilter" /> class.
    /// </summary>
    /// <param name="profanityDetector">The profanity detector service.</param>
    public BlacklistAudioFilter(ProfanityDetector profanityDetector)
        : this(profanityDetector, DEFAULT_BEEP_FREQUENCY) { }

    /// <summary>
    ///     Initializes a new instance of the <see cref="BlacklistAudioFilter" /> class with custom audio parameters.
    /// </summary>
    /// <param name="profanityDetector">The profanity detector service.</param>
    /// <param name="beepFrequency">The frequency of the beep tone in Hz.</param>
    /// <param name="beepVolume">The volume of the beep tone (0.0-1.0).</param>
    /// <exception cref="ArgumentNullException">Thrown when profanityDetector is null.</exception>
    public BlacklistAudioFilter(
        ProfanityDetector profanityDetector,
        float             beepFrequency = DEFAULT_BEEP_FREQUENCY,
        float             beepVolume    = DEFAULT_BEEP_VOLUME)
    {
        _profanityDetector = profanityDetector ?? throw new ArgumentNullException(nameof(profanityDetector));
        _beepFrequency     = beepFrequency;
        _beepVolume        = Math.Clamp(beepVolume, 0.0f, 1.0f);

        // Load profanity dictionary
        var profanityListPath = ModelUtils.GetModelPath(ModelType.BadWords);
        _profanityDictionary = LoadProfanityDictionary(profanityListPath);

        // Pre-calculate one cycle of the beep tone for efficient reuse
        // Using a square wave instead of sine wave for a more traditional censorship beep
        var samplesPerCycle = (int)(REFERENCE_SAMPLE_RATE / _beepFrequency);
        _beepCycle = new float[samplesPerCycle];

        for ( var i = 0; i < samplesPerCycle; i++ )
        {
            // Generate square wave (values are either +volume or -volume)
            _beepCycle[i] = i < samplesPerCycle / 2 ? _beepVolume : -_beepVolume;
        }
    }

    /// <summary>
    ///     Processes an audio segment to censor profane content with beep tones.
    /// </summary>
    /// <param name="segment">The audio segment to process.</param>
    public void Process(AudioSegment segment)
    {
        if ( segment == null || segment.AudioData.IsEmpty || segment.Tokens == null || segment.Tokens.Count == 0 )
        {
            return; // Early exit for invalid segments
        }

        var tokens     = segment.Tokens;
        var audioSpan  = segment.AudioData.Span;
        var sampleRate = segment.SampleRate;

        // First step: Identify and mark profane tokens
        MarkProfaneTokens(tokens);

        // Second step: Apply beep sound to marked tokens
        foreach ( var (token, index) in tokens.Select((t, i) => (t, i)) )
        {
            if ( token is not { Text: "[REDACTED]" } )
            {
                continue; // Skip null or non-redacted tokens
            }

            // Determine valid time boundaries for the audio replacement
            var (startTime, endTime) = DetermineTimeBoundaries(token, tokens, index);

            // Convert timestamps to sample indices
            var startIndex = (int)(startTime * sampleRate);
            var endIndex   = (int)(endTime * sampleRate);

            // Validate indices to prevent out-of-bounds access
            startIndex = Math.Max(0, startIndex);
            endIndex   = Math.Min(audioSpan.Length, endIndex);

            if ( startIndex >= endIndex || startIndex >= audioSpan.Length )
            {
                continue; // Invalid range
            }

            // Apply beep tone to the segment
            ApplyBeepTone(audioSpan, startIndex, endIndex, sampleRate);
        }
    }

    public int Priority => -100;

    /// <summary>
    ///     Marks tokens containing profanity based on the overall severity level.
    /// </summary>
    /// <param name="tokens">The list of tokens to process.</param>
    /// <param name="tolerance">The tolerance level for profanity.</param>
    private void MarkProfaneTokens(IReadOnlyList<Token> tokens, ProfanitySeverity tolerance = ProfanitySeverity.Clean)
    {
        if ( tokens.Count == 0 )
        {
            return;
        }

        // Build the full sentence from tokens for overall evaluation
        var sentence = BuildSentenceFromTokens(tokens);

        // Evaluate overall profanity severity
        var overallSeverity = _profanityDetector.EvaluateProfanity(sentence);

        // If the overall severity is within tolerance, no need to censor
        if ( overallSeverity <= tolerance )
        {
            return;
        }

        // Mark individual profane tokens
        foreach ( var token in tokens )
        {
            if ( token == null || string.IsNullOrWhiteSpace(token.Text) )
            {
                continue;
            }

            // Split the token into words to check each individually
            var words = SplitIntoWords(token.Text);

            foreach ( var word in words )
            {
                if ( IsProfaneWord(word) )
                {
                    token.Text = "[REDACTED]";

                    break; // Break once we find any profanity in the token
                }
            }
        }
    }

    /// <summary>
    ///     Splits text into individual words, preserving only alphanumeric characters.
    /// </summary>
    private static IEnumerable<string> SplitIntoWords(string text)
    {
        if ( string.IsNullOrWhiteSpace(text) )
        {
            yield break;
        }

        // Use regex to split text into words (sequences of letters)
        var matches = Regex.Matches(text, @"\b[a-zA-Z]+\b");

        foreach ( Match match in matches )
        {
            yield return match.Value;
        }
    }

    /// <summary>
    ///     Builds a complete sentence from a list of tokens.
    /// </summary>
    private static string BuildSentenceFromTokens(IReadOnlyList<Token> tokens)
    {
        if ( tokens.Count == 0 )
        {
            return string.Empty;
        }

        var builder = new StringBuilder();

        // Add all tokens except the last one with their whitespace
        for ( var i = 0; i < tokens.Count - 1; i++ )
        {
            if ( tokens[i] == null || tokens[i].Text == null )
            {
                continue;
            }

            builder.Append(tokens[i].Text);
            if ( tokens[i].Whitespace == " " )
            {
                builder.Append(' ');
            }
        }

        // Add the last token
        if ( tokens[^1] != null && tokens[^1].Text != null )
        {
            builder.Append(tokens[^1].Text);
        }

        return builder.ToString();
    }

    /// <summary>
    ///     Determines the start and end time boundaries for audio replacement.
    /// </summary>
    private (double Start, double End) DetermineTimeBoundaries(Token token, IReadOnlyList<Token> allTokens, int tokenIndex)
    {
        if ( token == null )
        {
            return (0, 0); // Default for null tokens
        }

        double startTime,
               endTime;

        // Handle the case where this is the first token with missing timestamps
        if ( tokenIndex == 0 && (!token.StartTs.HasValue || !token.EndTs.HasValue) )
        {
            startTime = 0;

            // For end time, use token's end time if available, otherwise use a reasonable default
            endTime = token.EndTs ?? EstimateTokenDuration(token);
        }
        else
        {
            // Determine start time
            if ( !token.StartTs.HasValue )
            {
                // If no start time is available, use previous token's end time if possible
                if ( tokenIndex > 0 && allTokens[tokenIndex - 1] != null )
                {
                    var prevToken = allTokens[tokenIndex - 1];
                    startTime = prevToken.EndTs ?? prevToken.StartTs ?? 0;
                }
                else
                {
                    startTime = 0;
                }
            }
            else
            {
                startTime = token.StartTs.Value;
            }

            // Determine end time
            if ( !token.EndTs.HasValue )
            {
                // If no end time is available, check if we can use the next token's start time
                if ( tokenIndex < allTokens.Count - 1 && allTokens[tokenIndex + 1] != null )
                {
                    var nextToken = allTokens[tokenIndex + 1];
                    endTime = nextToken.StartTs ?? startTime + EstimateTokenDuration(token);
                }
                else
                {
                    // For the last token, estimate a reasonable duration
                    endTime = startTime + EstimateTokenDuration(token);
                }
            }
            else
            {
                endTime = token.EndTs.Value;
            }
        }

        // Ensure we always have a positive duration
        if ( endTime <= startTime )
        {
            endTime = startTime + 0.1; // Add minimal duration if times are invalid
        }

        return (startTime, endTime);
    }

    /// <summary>
    ///     Estimates a reasonable duration for a token based on its content length.
    /// </summary>
    private double EstimateTokenDuration(Token token)
    {
        if ( token == null )
        {
            return 0.1; // Default for null tokens
        }

        // Average speaking rate is roughly 5-6 characters per second
        const double charsPerSecond = 5.5;

        // Minimum duration to ensure even single characters get enough time
        const double minimumDuration = 0.1;

        return Math.Max(minimumDuration, (token.Text?.Length ?? 0) / charsPerSecond);
    }

    /// <summary>
    ///     Applies a beep tone to a section of audio with smooth fade-in and fade-out.
    /// </summary>
    private void ApplyBeepTone(Span<float> audioData, int startIndex, int endIndex, int sampleRate)
    {
        var length = endIndex - startIndex;
        if ( length <= 0 )
        {
            return;
        }

        // Calculate how to map our pre-calculated beep cycle to the current sample rate
        var cycleScaleFactor = (double)sampleRate / REFERENCE_SAMPLE_RATE;

        // Number of samples for one complete cycle at the current sample rate
        var samplesPerCycle = (int)(_beepCycle.Length * cycleScaleFactor);

        // Make sure we have at least one sample per cycle
        samplesPerCycle = Math.Max(1, samplesPerCycle);

        // Apply a short fade-in and fade-out to avoid clicks
        // 5ms fade at current sample rate or 1/4 of segment length, whichever is smaller
        var fadeLength = Math.Min((int)(0.005 * sampleRate), length / 4);
        fadeLength = Math.Max(1, fadeLength); // Ensure at least 1 sample for fade

        for ( var i = 0; i < length; i++ )
        {
            // Calculate the position in the beep cycle
            var cycleIndex = (int)(i % samplesPerCycle / cycleScaleFactor) % _beepCycle.Length;

            // Get beep sample from pre-calculated cycle (with bounds check)
            var beepSample = _beepCycle[Math.Max(0, Math.Min(cycleIndex, _beepCycle.Length - 1))];

            // Apply fade-in and fade-out for smoother audio transitions
            var fadeMultiplier = 1.0f;
            if ( i < fadeLength )
            {
                fadeMultiplier = (float)i / fadeLength; // Linear fade-in
            }
            else if ( i > length - fadeLength )
            {
                fadeMultiplier = (float)(length - i) / fadeLength; // Linear fade-out
            }

            // Replace the original sample with the beep
            audioData[startIndex + i] = beepSample * fadeMultiplier;
        }
    }

    /// <summary>
    ///     Checks if the given word is profane, including checking for variations.
    /// </summary>
    private bool IsProfaneWord(string word)
    {
        if ( string.IsNullOrWhiteSpace(word) )
        {
            return false;
        }

        // Check cache first to avoid redundant processing
        if ( _wordCheckCache.TryGetValue(word, out var result) )
        {
            return result;
        }

        // Normalize the word
        var normalized = NormalizeText(word);

        // Direct match check
        if ( _profanityDictionary.Contains(normalized) )
        {
            _wordCheckCache[word] = true;

            return true;
        }

        // Check for variations with common suffixes
        foreach ( var suffix in COMMON_SUFFIXES )
        {
            if ( normalized.EndsWith(suffix, StringComparison.OrdinalIgnoreCase) )
            {
                // Try removing the suffix and check if the base word is profane
                var baseWord = normalized.Substring(0, normalized.Length - suffix.Length);

                // Only check non-empty base words
                if ( !string.IsNullOrEmpty(baseWord) && _profanityDictionary.Contains(baseWord) )
                {
                    _wordCheckCache[word] = true;

                    return true;
                }
            }
        }

        // Word not found to be profane after all checks
        _wordCheckCache[word] = false;

        return false;
    }

    /// <summary>
    ///     Normalizes text by removing diacritics, punctuation, and converting to lowercase.
    /// </summary>
    private static string NormalizeText(string text)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return string.Empty;
        }

        // Convert to lowercase and normalize diacritics
        var normalized = text.ToLowerInvariant().Normalize(NormalizationForm.FormD);

        // Remove diacritical marks and punctuation in a single pass for efficiency
        var sb = new StringBuilder(normalized.Length);
        foreach ( var c in normalized )
        {
            // Keep only characters that are not diacritics or punctuation
            if ( CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark && !char.IsPunctuation(c) )
            {
                sb.Append(c);
            }
        }

        return sb.ToString().Trim();
    }

    /// <summary>
    ///     Loads the profanity dictionary from a file with error handling.
    /// </summary>
    private static HashSet<string> LoadProfanityDictionary(string filePath)
    {
        try
        {
            if ( string.IsNullOrEmpty(filePath) || !File.Exists(filePath) )
            {
                Console.Error.WriteLine($"Warning: Profanity list file not found at: {filePath}");

                return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            }

            return new HashSet<string>(
                                       File.ReadAllLines(filePath)
                                           .Where(line => !string.IsNullOrWhiteSpace(line))
                                           .Select(word => NormalizeText(word.Trim())),
                                       StringComparer.OrdinalIgnoreCase
                                      );
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"Error loading profanity dictionary: {ex.Message}");

            return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        }
    }
}

using System.Buffers;

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents an audio source that stores a continuous memory with only silence and returns it when needed.
/// </summary>
public sealed class SilenceAudioSource : IAudioSource
{
    private readonly Lazy<byte[]> byteSilence;

    private readonly Lazy<float[]> floatSilence;

    private readonly bool useMemoryPool;

    public SilenceAudioSource(TimeSpan duration, uint sampleRate, IReadOnlyDictionary<string, string> metadata, ushort channelCount = 1, ushort bitsPerSample = 16, bool useMemoryPool = true)
    {
        SampleRate         = sampleRate;
        Metadata           = metadata;
        ChannelCount       = channelCount;
        BitsPerSample      = bitsPerSample;
        this.useMemoryPool = useMemoryPool;
        Duration           = duration;
        FramesCount        = (long)(duration.TotalSeconds * sampleRate);

        byteSilence  = new Lazy<byte[]>(GenerateByteSilence);
        floatSilence = new Lazy<float[]>(GenerateFloatSilence);
    }

    public IReadOnlyDictionary<string, string> Metadata { get; }

    public TimeSpan Duration { get; }

    public TimeSpan TotalDuration => Duration;

    public uint SampleRate { get; }

    public long FramesCount { get; }

    public ushort ChannelCount { get; }

    public bool IsInitialized => true;

    public ushort BitsPerSample { get; }

    public void Dispose()
    {
        if ( useMemoryPool )
        {
            // We don't want to clear this as it is silence anyway (no user data to clear)
            if ( byteSilence.IsValueCreated )
            {
                ArrayPool<byte>.Shared.Return(byteSilence.Value);
            }

            if ( floatSilence.IsValueCreated )
            {
                ArrayPool<float>.Shared.Return(floatSilence.Value, true);
            }
        }
    }

    public Task<Memory<byte>> GetFramesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var frameSize   = ChannelCount * BitsPerSample / 8;
        var startIndex  = (int)(startFrame * frameSize);
        var lengthBytes = (int)(Math.Min(maxFrames, FramesCount - startFrame) * frameSize);

        var slice = byteSilence.Value.AsMemory(startIndex, lengthBytes);

        return Task.FromResult(slice);
    }

    public Task<Memory<float>> GetSamplesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var startIndex    = (int)(startFrame * ChannelCount);
        var lengthSamples = (int)(Math.Min(maxFrames, FramesCount - startFrame) * ChannelCount);

        var slice = floatSilence.Value.AsMemory(startIndex, lengthSamples);

        return Task.FromResult(slice);
    }

    public Task<int> CopyFramesAsync(Memory<byte> destination, long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var frameSize   = ChannelCount * BitsPerSample / 8;
        var startIndex  = (int)(startFrame * frameSize);
        var lengthBytes = (int)(Math.Min(maxFrames, FramesCount - startFrame) * frameSize);

        var slice = byteSilence.Value.AsMemory(startIndex, lengthBytes);
        slice.CopyTo(destination);
        var frames = lengthBytes / frameSize;

        return Task.FromResult(frames);
    }

    private byte[] GenerateByteSilence()
    {
        var frameSize  = ChannelCount * BitsPerSample / 8;
        var totalBytes = FramesCount * frameSize;

        var silence = useMemoryPool ? ArrayPool<byte>.Shared.Rent((int)totalBytes) : new byte[totalBytes];

        // 8-bit PCM silence is centered at 128, not 0
        if ( BitsPerSample == 8 )
        {
            Array.Fill<byte>(silence, 128, 0, (int)totalBytes);
        }
        else
        {
            Array.Clear(silence, 0, (int)totalBytes); // Zero out for non-8-bit audio
        }

        return silence;
    }

    private float[] GenerateFloatSilence()
    {
        var totalSamples = FramesCount * ChannelCount;
        var silence      = useMemoryPool ? ArrayPool<float>.Shared.Rent((int)totalSamples) : new float[totalSamples];

        Array.Clear(silence, 0, (int)totalSamples); // Silence is zeroed-out memory

        return silence;
    }
}

using System.Collections.Concurrent;
using System.Diagnostics;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Enhanced memory-efficient TTS cache implementation with expiration and metrics
/// </summary>
public class TtsMemoryCache : ITtsCache, IAsyncDisposable
{
    private readonly ConcurrentDictionary<string, CacheItem> _cache = new();

    private readonly Timer _cleanupTimer;

    private readonly ILogger<TtsMemoryCache> _logger;

    private readonly TtsCacheOptions _options;

    private readonly ConcurrentDictionary<string, Stopwatch> _pendingOperations = new();

    private bool _disposed;

    private long _evictions;

    // Metrics
    private long _hits;

    private long _misses;

    public TtsMemoryCache(
        ILogger<TtsMemoryCache>   logger,
        IOptions<TtsCacheOptions> options = null)
    {
        _logger  = logger ?? throw new ArgumentNullException(nameof(logger));
        _options = options?.Value ?? new TtsCacheOptions();

        // Set up periodic cleanup if expiration is enabled
        if ( _options.ItemExpiration.HasValue )
        {
            // Run cleanup at half the expiration interval or at least every minute
            var cleanupInterval = TimeSpan.FromMilliseconds(
                                                            Math.Max(_options.ItemExpiration.Value.TotalMilliseconds / 2, 60000));

            _cleanupTimer = new Timer(CleanupExpiredItems, null, cleanupInterval, cleanupInterval);
        }
    }

    /// <summary>
    ///     Gets or adds an item to the cache with metrics and expiration support
    /// </summary>
    public async Task<T> GetOrAddAsync<T>(
        string                           key,
        Func<CancellationToken, Task<T>> valueFactory,
        CancellationToken                cancellationToken = default) where T : class
    {
        // Argument validation
        if ( string.IsNullOrEmpty(key) )
        {
            throw new ArgumentException("Cache key cannot be null or empty", nameof(key));
        }

        if ( valueFactory == null )
        {
            throw new ArgumentNullException(nameof(valueFactory));
        }

        // Check for cancellation
        cancellationToken.ThrowIfCancellationRequested();

        // Check if the item exists in cache and is valid
        if ( _cache.TryGetValue(key, out var cacheItem) )
        {
            if ( cacheItem.Value is T typedValue && !IsExpired(cacheItem) )
            {
                Interlocked.Increment(ref _hits);
                _logger.LogDebug("Cache hit for key {Key}", key);

                return typedValue;
            }

            // Item exists but is either wrong type or expired - remove it
            _cache.TryRemove(key, out _);
        }

        // Item not in cache or was invalid, create it
        Interlocked.Increment(ref _misses);
        _logger.LogDebug("Cache miss for key {Key}, creating new value", key);

        var stopwatch = new Stopwatch();
        if ( _options.CollectMetrics )
        {
            _pendingOperations[key] = stopwatch;
            stopwatch.Start();
        }

        try
        {
            // Create the value
            var value = await valueFactory(cancellationToken);

            if ( value == null )
            {
                _logger.LogWarning("Value factory for key {Key} returned null", key);

                return null;
            }

            // Enforce maximum cache size if configured
            EnforceSizeLimit();

            // Add to cache using GetOrAdd to handle the case where another thread might have
            // added the same key while we were creating the value
            var newItem    = new CacheItem { Value = value };
            var actualItem = _cache.GetOrAdd(key, newItem);

            // If another thread beat us to it and that value is of the correct type, use it
            if ( actualItem != newItem && actualItem.Value is T existingValue && !IsExpired(actualItem) )
            {
                _logger.LogDebug("Another thread already added key {Key}", key);

                return existingValue;
            }

            return value;
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            _logger.LogError(ex, "Error creating value for key {Key}", key);

            throw;
        }
        finally
        {
            if ( _options.CollectMetrics && _pendingOperations.TryRemove(key, out var sw) )
            {
                sw.Stop();
                _logger.LogDebug("Value creation for key {Key} took {ElapsedMs}ms", key, sw.ElapsedMilliseconds);
            }
        }
    }

    /// <summary>
    ///     Removes an item from the cache
    /// </summary>
    public void Remove(string key)
    {
        if ( string.IsNullOrEmpty(key) )
        {
            throw new ArgumentException("Cache key cannot be null or empty", nameof(key));
        }

        if ( _cache.TryRemove(key, out _) )
        {
            _logger.LogDebug("Removed item with key {Key} from cache", key);
        }
    }

    /// <summary>
    ///     Clears the cache
    /// </summary>
    public void Clear()
    {
        _cache.Clear();
        _logger.LogInformation("Cache cleared");
    }

    /// <summary>
    ///     Disposes the cache resources
    /// </summary>
    public async ValueTask DisposeAsync()
    {
        if ( _disposed )
        {
            return;
        }

        _disposed = true;

        _cleanupTimer?.Dispose();
        _cache.Clear();

        var stats = GetStatistics();
        _logger.LogInformation(
                               "Cache disposed. Final stats: Size={Size}, Hits={Hits}, Misses={Misses}, Evictions={Evictions}",
                               stats.Size, stats.Hits, stats.Misses, stats.Evictions);

        await Task.CompletedTask;
    }

    /// <summary>
    ///     Gets the current cache statistics
    /// </summary>
    public (int Size, long Hits, long Misses, long Evictions) GetStatistics() { return (_cache.Count, _hits, _misses, _evictions); }

    /// <summary>
    ///     Determines if a cache item has expired
    /// </summary>
    private bool IsExpired(CacheItem item)
    {
        if ( _options.ItemExpiration == null )
        {
            return false;
        }

        return DateTime.UtcNow - item.CreatedAt > _options.ItemExpiration.Value;
    }

    /// <summary>
    ///     Enforces the maximum cache size by removing oldest items if necessary
    /// </summary>
    private void EnforceSizeLimit()
    {
        if ( _options.MaxItems <= 0 || _cache.Count < _options.MaxItems )
        {
            return;
        }

        // Get all items with their ages
        var items = _cache.ToArray();

        // Sort by creation time (oldest first)
        Array.Sort(items, (a, b) => a.Value.CreatedAt.CompareTo(b.Value.CreatedAt));

        // Remove oldest items to get back under the limit
        // We'll remove 10% of max capacity to avoid doing this too frequently
        var toRemove = Math.Max(1, _options.MaxItems / 10);

        for ( var i = 0; i < toRemove && i < items.Length; i++ )
        {
            if ( _cache.TryRemove(items[i].Key, out _) )
            {
                Interlocked.Increment(ref _evictions);
            }
        }

        _logger.LogInformation("Removed {Count} items from cache due to size limit", toRemove);
    }

    /// <summary>
    ///     Periodically removes expired items from the cache
    /// </summary>
    private void CleanupExpiredItems(object state)
    {
        if ( _disposed )
        {
            return;
        }

        try
        {
            var removed = 0;
            foreach ( var key in _cache.Keys )
            {
                if ( _cache.TryGetValue(key, out var item) && IsExpired(item) )
                {
                    if ( _cache.TryRemove(key, out _) )
                    {
                        removed++;
                        Interlocked.Increment(ref _evictions);
                    }
                }
            }

            if ( removed > 0 )
            {
                _logger.LogInformation("Removed {Count} expired items from cache", removed);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during cache cleanup");
        }
    }

    private class CacheItem
    {
        public object Value { get; set; }

        public DateTime CreatedAt { get; } = DateTime.UtcNow;
    }
}

using System.Collections.Concurrent;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Audio.Player;

/// <summary>
///     Default implementation of IAudioBufferManager.
/// </summary>
public class DefaultAudioBufferManager : IAudioBufferManager
{
    private readonly BlockingCollection<(Memory<float> Data, AudioSegment Segment)> _audioQueue;

    private readonly ILogger _logger;

    private readonly int _maxBufferCount;

    private bool _disposed;

    public DefaultAudioBufferManager(int maxBufferCount = 32, ILogger<DefaultAudioBufferManager>? logger = null)
    {
        _maxBufferCount = maxBufferCount > 0
                              ? maxBufferCount
                              : throw new ArgumentException("Max buffer count must be positive", nameof(maxBufferCount));

        _logger     = logger ?? NullLogger<DefaultAudioBufferManager>.Instance;
        _audioQueue = new BlockingCollection<(Memory<float>, AudioSegment)>(_maxBufferCount);
    }

    public int BufferCount => _audioQueue.Count;

    public bool ProducerCompleted { get; private set; }

    public async Task EnqueueSegmentsAsync(
        IAsyncEnumerable<AudioSegment> audioSegments,
        CancellationToken              cancellationToken)
    {
        try
        {
            ProducerCompleted = false;

            await foreach ( var segment in audioSegments.WithCancellation(cancellationToken) )
            {
                if ( segment.AudioData.Length <= 0 || cancellationToken.IsCancellationRequested )
                {
                    continue;
                }

                try
                {
                    // Add to bounded collection - will block if queue is full (applying backpressure)
                    _audioQueue.Add((segment.AudioData, segment), cancellationToken);
                }
                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                {
                    // Expected when cancellation is requested
                    break;
                }
                catch (InvalidOperationException) when (_audioQueue.IsAddingCompleted)
                {
                    // Queue was completed during processing
                    break;
                }
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            _logger.LogInformation("Audio enqueuing cancelled");

            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error while enqueuing audio segments");

            throw;
        }
        finally
        {
            ProducerCompleted = true;
        }
    }

    public bool TryGetNextBuffer(
        out (Memory<float> Data, AudioSegment Segment) buffer,
        int                                            timeoutMs,
        CancellationToken                              cancellationToken)
    {
        return _audioQueue.TryTake(out buffer, timeoutMs, cancellationToken);
    }

    public void Clear()
    {
        while ( _audioQueue.TryTake(out _) ) { }
    }

    public async ValueTask DisposeAsync()
    {
        if ( _disposed )
        {
            return;
        }

        _disposed = true;

        try
        {
            _audioQueue.Dispose();
            await Task.CompletedTask; // For async consistency
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error disposing audio buffer manager");
        }
    }
}

using System.Diagnostics;
using System.Drawing;
using System.Numerics;

using Hexa.NET.ImGui;
using Hexa.NET.ImGui.Widgets;

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.UI.GUI;

public class RouletteWheelEditor : ConfigSectionEditorBase
{
    private readonly string[] _anchorOptions = { "TopLeft", "TopCenter", "TopRight", "MiddleLeft", "MiddleCenter", "MiddleRight", "BottomLeft", "BottomCenter", "BottomRight" };

    private readonly FontProvider _fontProvider;

    private readonly string[] _positionOptions = { "Center", "Custom Position", "Anchor Position" };

    private readonly IUiThemeManager _themeManager;

    private readonly RouletteWheel.RouletteWheel _wheel;

    private bool _animateToggle;

    private float _animationDuration;

    private List<string> _availableFonts = new();

    private RouletteWheelOptions _currentConfig;

    private bool _defaultEnabled;

    private string _fontName;

    private int _fontSize;

    private int _height;

    private bool _loadingFonts = false;

    private float _minRotations;

    private string _newSectionLabel = string.Empty;

    private bool _radialTextOrientation = true;

    private float _rotationDegrees;

    private string[] _sectionLabels;

    private int _selectedAnchor = 4;

    private int _selectedPositionOption = 0;

    private int _selectedSection = 0;

    private float _spinDuration;

    private ActiveOperation? _spinningOperation = null;

    private int _strokeWidth;

    private string _textColor;

    private float _textScale;

    private bool _useAdaptiveSize = true;

    private int _width;

    private float _xPosition = 0.5f;

    private float _yPosition = 0.5f;

    public RouletteWheelEditor(
        IUiConfigurationManager     configManager,
        IEditorStateManager         stateManager,
        IUiThemeManager             themeManager,
        RouletteWheel.RouletteWheel wheel,
        FontProvider                fontProvider)
        : base(configManager, stateManager)
    {
        _wheel        = wheel;
        _themeManager = themeManager;
        _fontProvider = fontProvider;

        _currentConfig = ConfigManager.GetConfiguration<RouletteWheelOptions>("RouletteWheel");
        LoadConfiguration(_currentConfig);
    }

    public override string SectionKey => "Roulette";

    public override string DisplayName => "Roulette Configuration";

    public override void Initialize() { LoadAvailableFontsAsync(); }

    private async void Spin(int? targetSection = null)
    {
        if ( _wheel.IsSpinning )
        {
            return;
        }

        try
        {
            _spinningOperation = new ActiveOperation("wheel-spinning", "Spinning Wheel");
            StateManager.RegisterActiveOperation(_spinningOperation);
            await _wheel.SpinAsync(targetSection);
        }
        catch (OperationCanceledException)
        {
            Debug.WriteLine("Spinning cancelled");
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error during spinning: {ex.Message}");

            if ( _spinningOperation != null )
            {
                StateManager.ClearActiveOperation(_spinningOperation.Id);
                _spinningOperation = null;
            }
        }
    }

    private async void LoadAvailableFontsAsync()
    {
        try
        {
            _loadingFonts = true;

            var operation = new ActiveOperation("load-fonts", "Loading Fonts");
            StateManager.RegisterActiveOperation(operation);

            var fonts = await _fontProvider.GetAvailableFontsAsync();
            _availableFonts = fonts.ToList();

            // Clear operation
            StateManager.ClearActiveOperation(operation.Id);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error loading fonts: {ex.Message}");
            _availableFonts = [];
        }
        finally
        {
            _loadingFonts = false;
        }
    }

    private void LoadConfiguration(RouletteWheelOptions config)
    {
        _width                 = config.Width;
        _height                = config.Height;
        _fontName              = config.Font;
        _fontSize              = config.FontSize;
        _textScale             = config.TextScale;
        _textColor             = config.TextColor;
        _strokeWidth           = config.TextStroke;
        _useAdaptiveSize       = config.AdaptiveText;
        _radialTextOrientation = config.RadialTextOrientation;
        _sectionLabels         = config.SectionLabels;
        _spinDuration          = config.SpinDuration;
        _minRotations          = config.MinRotations;
        _animateToggle         = config.AnimateToggle;
        _animationDuration     = config.AnimationDuration;
        _defaultEnabled        = config.Enabled;
    }

    public override void Render()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        if ( ImGui.BeginTabBar("RouletteConfigTabs") )
        {
            if ( ImGui.BeginTabItem("Basic Settings") )
            {
                RenderTestingSection();
                RenderBasicSettingsTab();
                RenderSectionsTab();
                RenderBehaviorTab();
                RenderStyleTab();

                ImGui.EndTabItem();
            }

            RenderPositioningTab();

            ImGui.EndTabBar();
        }

        ImGui.Spacing();
        ImGui.Separator();
        ImGui.Spacing();

        ImGui.SetCursorPosX(availWidth * .5f * .5f);
        if ( ImGui.Button("Reset", new Vector2(150, 0)) )
        {
            ResetToDefaults();
        }

        ImGui.SameLine(0, 10);

        if ( ImGui.IsItemHovered() )
        {
            ImGui.SetTooltip("Reset all Wheel settings to default values");
        }

        if ( !StateManager.HasUnsavedChanges )
        {
            ImGui.BeginDisabled();
            ImGui.Button("Save", new Vector2(150, 0));
            ImGui.EndDisabled();
        }
        else if ( ImGui.Button("Save", new Vector2(150, 0)) )
        {
            SaveConfiguration();
        }
    }

    private void RenderBasicSettingsTab()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;
        bool widthChanged,
             heightChanged;

        ImGui.Spacing();
        ImGui.SeparatorText("Wheel Dimensions");
        ImGui.Spacing();

        ImGui.SetNextItemWidth(125);
        widthChanged = ImGui.InputInt("Width##WheelWidth", ref _width, 10);
        if ( widthChanged )
        {
            _width = Math.Max(50, _width); // Minimum width
        }

        ImGui.SameLine();

        ImGui.SetCursorPosX(availWidth - 175);
        if ( ImGui.Button("Equal Dimensions", new Vector2(175, 0)) )
        {
            var size         = Math.Max(_width, _height);
            _width = _height = size;
            _wheel.SetDiameter(size);
            widthChanged  = true;
            heightChanged = true;
        }

        ImGui.SetNextItemWidth(125);
        heightChanged = ImGui.InputInt("Height##WheelHeight", ref _height, 10);
        if ( heightChanged )
        {
            _height = Math.Max(50, _height); // Minimum height
        }

        if ( widthChanged || heightChanged )
        {
            UpdateConfiguration();
        }
    }

    private void RenderSectionsTab()
    {
        var availWidth      = ImGui.GetContentRegionAvail().X;
        var sectionsChanged = false;

        ImGui.Spacing();
        ImGui.SeparatorText("Wheel Sections");
        ImGui.Spacing();

        if ( _sectionLabels.Length > 0 )
        {
            ImGui.BeginChild("SectionsList", new Vector2(0, 0), ImGuiChildFlags.AutoResizeY);

            for ( var i = 0; i < _sectionLabels.Length; i++ )
            {
                ImGui.PushID(i);

                var tempLabel = _sectionLabels[i];
                ImGui.SetNextItemWidth(availWidth - 10 - 30);
                if ( ImGui.InputText("##SectionLabel", ref tempLabel, 128) )
                {
                    _sectionLabels[i] = tempLabel;
                    sectionsChanged   = true;
                }

                ImGui.SameLine(0, 10);

                if ( ImGui.Button("X##RemoveSection" + i, new Vector2(30, 0)) )
                {
                    var newLabels = new string[_sectionLabels.Length - 1];
                    Array.Copy(_sectionLabels, 0, newLabels, 0, i);
                    Array.Copy(_sectionLabels, i + 1, newLabels, i, _sectionLabels.Length - i - 1);
                    _sectionLabels  = newLabels;
                    sectionsChanged = true;
                }

                ImGui.PopID();
            }

            ImGui.EndChild();
        }
        else
        {
            TextHelper.TextCenteredH("No sections defined. Add your first section below.");
        }

        // Add new section
        ImGui.Spacing();
        // ImGui.Separator();
        ImGui.Spacing();

        ImGui.SetNextItemWidth(availWidth - 80 - 10);
        ImGui.InputText("##NewSectionLabel", ref _newSectionLabel, 128);

        ImGui.SameLine(0, 10);

        if ( ImGui.Button("Add", new Vector2(80, 0)) && !string.IsNullOrWhiteSpace(_newSectionLabel) )
        {
            // Add the new section
            var newLabels = new string[(_sectionLabels?.Length ?? 0) + 1];
            if ( _sectionLabels is { Length: > 0 } )
            {
                Array.Copy(_sectionLabels, newLabels, _sectionLabels.Length);
            }

            newLabels[^1]    = _newSectionLabel;
            _sectionLabels   = newLabels;
            _newSectionLabel = string.Empty;
            sectionsChanged  = true;
        }

        if ( sectionsChanged )
        {
            UpdateConfiguration();
        }
    }

    private void RenderStyleTab()
    {
        var availWidth    = ImGui.GetContentRegionAvail().X;
        var configChanged = false;

        ImGui.Spacing();
        ImGui.SeparatorText("Text Appearance");
        ImGui.Spacing();

        // Font selection with refresh button
        {
            ImGui.SetNextItemWidth(availWidth - 120);

            if ( _loadingFonts )
            {
                ImGui.BeginDisabled();
                var loadingText = "Loading fonts...";
                ImGui.InputText("##Font", ref loadingText, 100, ImGuiInputTextFlags.ReadOnly);
                ImGui.EndDisabled();
            }
            else
            {
                var fontChanged = false;
                if ( ImGui.BeginCombo("##Font", string.IsNullOrEmpty(_fontName) ? "<Select font>" : _fontName) )
                {
                    if ( _availableFonts.Count == 0 )
                    {
                        ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 1.0f), "No fonts available");
                    }
                    else
                    {
                        foreach ( var font in _availableFonts )
                        {
                            var isSelected = font == _fontName;
                            if ( ImGui.Selectable(font, isSelected) )
                            {
                                _fontName   = font;
                                fontChanged = true;
                            }

                            if ( isSelected )
                            {
                                ImGui.SetItemDefaultFocus();
                            }
                        }
                    }

                    ImGui.EndCombo();
                }

                configChanged |= fontChanged;
            }

            ImGui.SameLine(0, 10);

            if ( ImGui.Button("Refresh", new Vector2(-1, 0)) )
            {
                LoadAvailableFontsAsync();
            }

            if ( ImGui.IsItemHovered() )
            {
                ImGui.SetTooltip("Refresh available fonts list");
            }
        }

        ImGui.Spacing();

        // ImGui.SetCursorPosX((availWidth - 110f - 200f - 110f - 200f) * .5f);
        // Create a 2-column table for consistent alignment
        if ( ImGui.BeginTable("StyleProperties", 4, ImGuiTableFlags.SizingFixedFit) )
        {
            ImGui.TableSetupColumn("1", 110f);
            ImGui.TableSetupColumn("2", 200f);
            ImGui.TableSetupColumn("3", 120f);
            ImGui.TableSetupColumn("4", 220f);

            // Font Size
            ImGui.TableNextRow();
            ImGui.TableNextColumn();
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Font Size");
            ImGui.TableNextColumn();
            var fontSizeChanged = ImGui.InputInt("##FontSize", ref _fontSize, 1);
            if ( fontSizeChanged )
            {
                _fontSize     = Math.Clamp(_fontSize, 1, 72);
                configChanged = true;
            }

            // Text Scale
            ImGui.TableNextColumn();
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Scale");
            ImGui.TableNextColumn();
            var scaleChanged = ImGui.SliderFloat("##Scale", ref _textScale, 0.5f, 2.0f, "%.1f");
            configChanged |= scaleChanged;

            // Text Color
            ImGui.TableNextRow();
            ImGui.TableNextColumn();
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Color");
            ImGui.TableNextColumn();
            var color        = ColorTranslator.FromHtml(_textColor);
            var colorVec     = new Vector4(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
            var colorChanged = ImGui.ColorEdit4("##Color", ref colorVec, ImGuiColorEditFlags.DisplayHex);
            if ( colorChanged )
            {
                _textColor    = $"#{(int)(colorVec.W * 255):X2}{(int)(colorVec.X * 255):X2}{(int)(colorVec.Y * 255):X2}{(int)(colorVec.Z * 255):X2}";
                configChanged = true;
            }

            // Stroke Width
            ImGui.TableNextColumn();
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Stroke Width");
            ImGui.TableNextColumn();
            var strokeChanged = ImGui.InputInt("##StrokeWidth", ref _strokeWidth, 1);
            if ( strokeChanged )
            {
                _strokeWidth  = Math.Clamp(_strokeWidth, 0, 5);
                configChanged = true;
            }

            ImGui.EndTable();
        }

        ImGui.Spacing();

        // Checkbox options in two columns
        var checkboxWidth    = availWidth / 2 - 10;
        var adaptSizeChanged = ImGui.Checkbox("Adaptive Size", ref _useAdaptiveSize);
        ImGui.SameLine(checkboxWidth + 43);
        var radialOrienChanged = ImGui.Checkbox("Radial Orientation", ref _radialTextOrientation);

        configChanged |= adaptSizeChanged | radialOrienChanged;

        // Apply configuration changes if needed
        if ( configChanged )
        {
            UpdateConfiguration();
        }
    }

    private void RenderPositioningTab()
    {
        if ( ImGui.BeginTabItem("Position & Rotation") )
        {
            ImGui.Text("Wheel Position");
            ImGui.Separator();

            // Position method selector
            ImGui.Combo("Position Method", ref _selectedPositionOption, _positionOptions, _positionOptions.Length);

            ImGui.Spacing();

            // Different UI based on position method
            switch ( _selectedPositionOption )
            {
                case 0: // Center
                    if ( ImGui.Button("Center in Viewport", new Vector2(200, 0)) )
                    {
                        _wheel.CenterInViewport();
                    }

                    break;

                case 1: // Custom Position
                    ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X * 0.4f);
                    ImGui.SliderFloat("X Position %", ref _xPosition, 0.0f, 1.0f, "%.2f");

                    ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X * 0.4f);
                    ImGui.SliderFloat("Y Position %", ref _yPosition, 0.0f, 1.0f, "%.2f");

                    if ( ImGui.Button("Apply Position", new Vector2(200, 0)) )
                    {
                        _wheel.PositionByPercentage(_xPosition, _yPosition);
                    }

                    break;

                case 2: // Anchor Position
                    ImGui.Combo("Anchor Point", ref _selectedAnchor, _anchorOptions, _anchorOptions.Length);

                    if ( ImGui.Button("Apply Anchor", new Vector2(200, 0)) )
                    {
                        // This would need a conversion from index to the actual ViewportAnchor enum
                        // For now, just showing the concept
                        // _wheel.PositionAt((ViewportAnchor)_selectedAnchor, new Vector2(0, 0));

                        // Simple implementation for demo purposes:
                        switch ( _selectedAnchor )
                        {
                            case 0:
                                _wheel.PositionByPercentage(0.0f, 0.0f);

                                break; // TopLeft
                            case 1:
                                _wheel.PositionByPercentage(0.5f, 0.0f);

                                break; // TopCenter
                            case 2:
                                _wheel.PositionByPercentage(1.0f, 0.0f);

                                break; // TopRight
                            case 3:
                                _wheel.PositionByPercentage(0.0f, 0.5f);

                                break; // MiddleLeft
                            case 4:
                                _wheel.PositionByPercentage(0.5f, 0.5f);

                                break; // MiddleCenter
                            case 5:
                                _wheel.PositionByPercentage(1.0f, 0.5f);

                                break; // MiddleRight
                            case 6:
                                _wheel.PositionByPercentage(0.0f, 1.0f);

                                break; // BottomLeft
                            case 7:
                                _wheel.PositionByPercentage(0.5f, 1.0f);

                                break; // BottomCenter
                            case 8:
                                _wheel.PositionByPercentage(1.0f, 1.0f);

                                break; // BottomRight
                        }
                    }

                    break;
            }

            ImGui.Spacing();
            ImGui.Separator();
            ImGui.Text("Wheel Rotation");

            // Display current rotation
            ImGui.Text($"Current Rotation: {_rotationDegrees:F1}°");

            // Rotation slider
            ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X * 0.7f);
            if ( ImGui.SliderFloat("Rotation (degrees)", ref _rotationDegrees, 0.0f, 360.0f, "%.1f°") )
            {
                _wheel.RotateByDegrees(_rotationDegrees - _wheel.RotationDegrees);
            }

            // Quick rotation buttons
            if ( ImGui.Button("+45°", new Vector2(60, 0)) )
            {
                _wheel.RotateByDegrees(45);
                _rotationDegrees = _wheel.RotationDegrees;
            }

            ImGui.SameLine();

            if ( ImGui.Button("+90°", new Vector2(60, 0)) )
            {
                _wheel.RotateByDegrees(90);
                _rotationDegrees = _wheel.RotationDegrees;
            }

            ImGui.SameLine();

            if ( ImGui.Button("-45°", new Vector2(60, 0)) )
            {
                _wheel.RotateByDegrees(-45);
                _rotationDegrees = _wheel.RotationDegrees;
            }

            ImGui.SameLine();

            if ( ImGui.Button("-90°", new Vector2(60, 0)) )
            {
                _wheel.RotateByDegrees(-90);
                _rotationDegrees = _wheel.RotationDegrees;
            }

            ImGui.EndTabItem();
        }
    }

    private void RenderBehaviorTab()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        ImGui.Spacing();
        ImGui.SeparatorText("Spin Configuration");
        ImGui.Spacing();

        // Spin duration
        ImGui.SetNextItemWidth(125);
        var spinDurationChanged = ImGui.SliderFloat("Spin Duration (sec)", ref _spinDuration, 1.0f, 10.0f, "%.1f");

        // Min rotations
        ImGui.SetNextItemWidth(125);
        var minRotChanged = ImGui.SliderFloat("Min Rotations", ref _minRotations, 1.0f, 10.0f, "%.0f");

        ImGui.Spacing();
        ImGui.SeparatorText("Animation Settings");
        ImGui.Spacing();

        ImGui.SetNextItemWidth(125);
        var aniDurChanged = ImGui.SliderFloat("Animation Duration (sec)", ref _animationDuration, 0.1f, 2.0f, "%.1f");

        ImGui.SameLine();
        ImGui.SetCursorPosX(availWidth - 200);

        // Animation toggle
        ImGui.SetNextItemWidth(200);
        var aniShowHideChanged = ImGui.Checkbox("Animate Show/Hide", ref _animateToggle);

        ImGui.SetCursorPosX(availWidth - 200);

        ImGui.SetNextItemWidth(200);
        var defaultEnabledChanged = ImGui.Checkbox("Default Enabled", ref _defaultEnabled);

        if ( spinDurationChanged || minRotChanged || aniDurChanged || aniShowHideChanged || defaultEnabledChanged )
        {
            UpdateConfiguration();
        }
    }

    private void RenderTestingSection()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        ImGui.Spacing();
        ImGui.SeparatorText("Playground");
        ImGui.Spacing();

        if ( _sectionLabels.Length > 0 )
        {
            ImGui.SetNextItemWidth(availWidth - 155);
            ImGui.Combo("##TargetSection", ref _selectedSection, _sectionLabels, _sectionLabels.Length);

            ImGui.SameLine(0, 5);

            if ( _wheel.IsSpinning )
            {
                ImGui.BeginDisabled();
                ImGui.Button("Spin to Selected", new Vector2(150, 0));
                ImGui.EndDisabled();
            }
            else if ( ImGui.Button("Spin to Selected", new Vector2(150, 0)) )
            {
                Spin(_selectedSection);
            }

            ImGui.Spacing();
            ImGui.SetCursorPosX(availWidth - 150);

            if ( _wheel.IsSpinning )
            {
                ImGui.BeginDisabled();
                ImGui.Button("Spin Random", new Vector2(150, 0));
                ImGui.EndDisabled();
            }
            else if ( ImGui.Button("Spin Random", new Vector2(150, 0)) )
            {
                Spin();
            }

            ImGui.SameLine(-1, 0);

            if ( _wheel.IsEnabled )
            {
                if ( ImGui.Button("Hide Wheel", new Vector2(155, 0)) )
                {
                    _wheel.Disable();
                }
            }
            else
            {
                if ( ImGui.Button("Show Wheel", new Vector2(155, 0)) )
                {
                    _wheel.Enable();
                }
            }

            if ( _wheel.IsSpinning )
            {
                ImGui.SameLine(0, 10);
                ImGui.SetNextItemWidth(availWidth - 155 - 150 - 10 - 5);
                ImGui.ProgressBar(_spinningOperation?.Progress ?? 0, new Vector2(0, 0), "Spinning...");
            }
        }
        else
        {
            var txt = "Add sections to test spinning";
            ImGui.SetCursorPosX((availWidth - ImGui.CalcTextSize(txt).X) * 0.5f);
            ImGui.TextColored(new Vector4(1, 0.3f, 0.3f, 1), txt);
        }
    }

    private void UpdateConfiguration()
    {
        var updatedConfig = _currentConfig with {
                                                    Font = _fontName,
                                                    FontSize = _fontSize,
                                                    TextColor = _textColor,
                                                    TextScale = _textScale,
                                                    TextStroke = _strokeWidth,
                                                    AdaptiveText = _useAdaptiveSize,
                                                    RadialTextOrientation = _radialTextOrientation,
                                                    SectionLabels = _sectionLabels,
                                                    SpinDuration = _spinDuration,
                                                    MinRotations = _minRotations,
                                                    Enabled = _defaultEnabled,
                                                    Width = _width,
                                                    Height = _height,
                                                    RotationDegrees = _rotationDegrees,
                                                    AnimateToggle = _animateToggle,
                                                    AnimationDuration = _animationDuration
                                                };

        _currentConfig = updatedConfig;
        ConfigManager.UpdateConfiguration(updatedConfig, SectionKey);

        MarkAsChanged();
    }

    public override void Update(float deltaTime)
    {
        if ( _spinningOperation != null && _wheel.IsSpinning )
        {
            _spinningOperation.Progress = _wheel.Progress;
        }
    }

    private void SaveConfiguration()
    {
        ConfigManager.SaveConfiguration();
        MarkAsSaved();
    }

    private void ResetToDefaults()
    {
        var defaultConfig = new RouletteWheelOptions();
        _currentConfig = defaultConfig;

        LoadConfiguration(_currentConfig);

        ConfigManager.UpdateConfiguration(defaultConfig, SectionKey);
        MarkAsChanged();
    }
}

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

namespace PersonaEngine.Lib.TTS.RVC;

public class ContentVec : IDisposable
{
    private readonly InferenceSession _model;

    public ContentVec(string modelPath)
    {
        var options = new SessionOptions {
                                             EnableMemoryPattern    = true,
                                             ExecutionMode          = ExecutionMode.ORT_PARALLEL,
                                             InterOpNumThreads      = Environment.ProcessorCount,
                                             IntraOpNumThreads      = Environment.ProcessorCount,
                                             GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
                                             LogSeverityLevel       = OrtLoggingLevel.ORT_LOGGING_LEVEL_FATAL,
                                         };

        options.AppendExecutionProvider_CPU();

        _model = new InferenceSession(modelPath, options);
    }

    public void Dispose() { _model?.Dispose(); }

    public DenseTensor<float> Forward(ReadOnlyMemory<float> wav)
    {
        // Create input tensor without unnecessary copying
        var tensor  = new DenseTensor<float>(new[] { 1, 1, wav.Length });
        var wavSpan = wav.Span;

        if ( wav.Length == 2 )
        {
            // Handle division by 2 for each element
            tensor[0, 0, 0] = wavSpan[0] / 2;
            tensor[0, 0, 1] = wavSpan[1] / 2;
        }
        else
        {
            // Process normally for all other lengths
            for ( var i = 0; i < wav.Length; i++ )
            {
                tensor[0, 0, i] = wavSpan[i];
            }
        }

        var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(_model.InputMetadata.Keys.First(), tensor) };

        using var results = _model.Run(inputs);
        var       output  = results[0].AsTensor<float>();

        // Process the output tensor
        return RVCUtils.Transpose(output, 0, 2, 1);
    }
}

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using System.Text.Json;

using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.ChatCompletion;

using OpenAI.Chat;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

using ChatMessage = PersonaEngine.Lib.Core.Conversation.Abstractions.Context.ChatMessage;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Context;

public class ConversationContext : IConversationContext
{
    private readonly IConversationCleanupStrategy? _cleanupStrategy;

    private readonly Dictionary<string, StringBuilder> _currentMessageBuffers = new();

    private readonly List<InteractionTurn> _history = new();

    private readonly Lock _lock = new();

    private readonly IDisposable? _optionsMonitorRegistration;

    private readonly Dictionary<string, ParticipantInfo> _participants;

    private readonly HashSet<string> _participantsReadyToCommit = new();

    private Guid _currentTurnId = Guid.Empty;

    private HashSet<string> _currentTurnParticipantIds = new();

    private string? _currentVisualContext;

    private ConversationContextOptions _options;

    private bool _turnInterrupted = false;

    private DateTimeOffset _turnStartTime;

    public ConversationContext(
        IEnumerable<ParticipantInfo>                initialParticipants,
        IOptionsMonitor<ConversationContextOptions> optionsMonitor,
        IConversationCleanupStrategy?               cleanupStrategy = null)
    {
        _participants    = initialParticipants.ToDictionary(p => p.Id);
        _options         = optionsMonitor.CurrentValue;
        _cleanupStrategy = cleanupStrategy ?? new MaxTurnsCleanupStrategy(100);
        _optionsMonitorRegistration = optionsMonitor.OnChange(newOptions =>
                                                              {
                                                                  lock (_lock)
                                                                  {
                                                                      if ( newOptions.SystemPromptFile != _options.SystemPromptFile )
                                                                      {
                                                                          PromptUtils.ClearCache();
                                                                      }

                                                                      _options = newOptions;
                                                                  }
                                                              });
    }

    public event EventHandler? ConversationUpdated;

    public IReadOnlyDictionary<string, ParticipantInfo> Participants
    {
        get
        {
            lock (_lock)
            {
                return new ReadOnlyDictionary<string, ParticipantInfo>(_participants);
            }
        }
    }

    public IReadOnlyList<InteractionTurn> History
    {
        get
        {
            lock (_lock)
            {
                return _history.AsReadOnly();
            }
        }
    }

    public string? CurrentVisualContext
    {
        get
        {
            lock (_lock)
            {
                return _currentVisualContext;
            }
        }
        set
        {
            lock (_lock)
            {
                _currentVisualContext = value;
                OnConversationUpdated();
            }
        }
    }

    public bool TryAddParticipant(ParticipantInfo participant)
    {
        lock (_lock)
        {
            var added = _participants.TryAdd(participant.Id, participant);
            if ( added )
            {
                OnConversationUpdated();
            }

            return added;
        }
    }

    public bool TryRemoveParticipant(string participantId)
    {
        lock (_lock)
        {
            var removed = _participants.Remove(participantId);
            if ( removed )
            {
                // Optional: Decide if removing a participant should affect ongoing turns
                // e.g., remove them from _currentTurnParticipantIds if turn is active?
                // Current implementation doesn't automatically remove from active turn.
                OnConversationUpdated();
            }

            return removed;
        }
    }

    #region Cleanup

    public void ApplyCleanupStrategy()
    {
        if ( _cleanupStrategy == null )
        {
            return;
        }

        lock (_lock)
        {
            if ( _cleanupStrategy.Cleanup(_history, _options, _participants) )
            {
                OnConversationUpdated();
            }
        }
    }

    public void ClearHistory()
    {
        lock (_lock)
        {
            if ( _history.Count <= 0 )
            {
                return;
            }

            _history.Clear();
            OnConversationUpdated();
        }
    }

    public void Dispose() { Dispose(true); }

    protected virtual void Dispose(bool disposing)
    {
        if ( disposing )
        {
            _optionsMonitorRegistration?.Dispose();
        }
    }

    #endregion

    #region Private Helpers

    protected virtual void OnConversationUpdated()
    {
        var handler = ConversationUpdated;
        handler?.Invoke(this, EventArgs.Empty);
    }

    private void InternalAbortTurn(bool raiseEvent = true)
    {
        var wasActive = _currentTurnId != Guid.Empty;

        _currentTurnId = Guid.Empty;
        _currentMessageBuffers.Clear();
        _participantsReadyToCommit.Clear();
        _currentTurnParticipantIds.Clear();
        _turnInterrupted = false;

        if ( wasActive && raiseEvent )
        {
            OnConversationUpdated();
        }
    }

    private void FinalizeCurrentTurn()
    {
        if ( _currentTurnId == Guid.Empty )
        {
            return;
        }

        var turn = _history.FirstOrDefault(t => t.TurnId == _currentTurnId);

        if ( turn != null )
        {
            turn.EndTime        = DateTimeOffset.UtcNow;
            turn.WasInterrupted = _turnInterrupted;
        }

        _currentTurnId = Guid.Empty;
        _currentMessageBuffers.Clear();
        _participantsReadyToCommit.Clear();
        _currentTurnParticipantIds.Clear();
        _turnInterrupted = false;
    }

    private InteractionTurn? CreatePendingTurnSnapshot()
    {
        if ( _currentTurnId == Guid.Empty || _currentMessageBuffers.Count == 0 )
        {
            return null;
        }

        var pendingMessages = new List<ChatMessage>();
        foreach ( var (participantId, buffer) in _currentMessageBuffers )
        {
            var text = buffer.ToString();

            if ( string.IsNullOrWhiteSpace(text) || !_participants.TryGetValue(participantId, out var participantInfo) )
            {
                continue;
            }

            var snapshotMessage = new ChatMessage(
                                                  Guid.NewGuid(),
                                                  participantId,
                                                  participantInfo.Name,
                                                  text,
                                                  _turnStartTime,
                                                  true,
                                                  participantInfo.Role
                                                 );

            pendingMessages.Add(snapshotMessage);
        }

        if ( pendingMessages.Count == 0 )
        {
            return null;
        }

        return new InteractionTurn(
                                   _currentTurnId,
                                   _currentTurnParticipantIds,
                                   _turnStartTime,
                                   null,
                                   pendingMessages,
                                   _turnInterrupted
                                  );
    }

    private void AddMessageToSkHistory(ChatHistory skChatHistory, ChatMessage message)
    {
        var role    = message.Role;
        var content = message.Text;

        if ( role != ChatMessageRole.Assistant && role != ChatMessageRole.System )
        {
            content = $"[{message.ParticipantName}]{message.Text}";

            role = ChatMessageRole.User;
        }

        skChatHistory.AddMessage(GetAuthorRole(role), content);
    }

    private AuthorRole GetAuthorRole(ChatMessageRole role)
    {
        return role switch {
            ChatMessageRole.System => AuthorRole.System,
            ChatMessageRole.Assistant => AuthorRole.Assistant,
            ChatMessageRole.User => AuthorRole.User,
            ChatMessageRole.Developer => AuthorRole.Developer,
            ChatMessageRole.Tool => AuthorRole.Tool,
            ChatMessageRole.Function => AuthorRole.Tool,
            _ => throw new ArgumentOutOfRangeException(nameof(role), role, null)
        };
    }

    #endregion

    #region Message Management

    public bool TryUpdateMessage(Guid turnId, Guid messageId, string newText)
    {
        lock (_lock)
        {
            var turn    = _history.FirstOrDefault(t => t.TurnId == turnId);
            var message = turn?.Messages.FirstOrDefault(m => m.MessageId == messageId);
            if ( message != null && message.Text != newText )
            {
                message.Text = newText;
                OnConversationUpdated();

                return true;
            }

            return false;
        }
    }

    public bool TryDeleteMessage(Guid turnId, Guid messageId)
    {
        lock (_lock)
        {
            var turn = _history.FirstOrDefault(t => t.TurnId == turnId);
            if ( turn != null )
            {
                var messageToRemove = turn.Messages.FirstOrDefault(m => m.MessageId == messageId);
                if ( messageToRemove == null )
                {
                    return false;
                }

                var messageRemoved = turn.Messages.Remove(messageToRemove);
                var turnRemoved    = false;
                if ( turn.Messages.Count == 0 )
                {
                    turnRemoved = _history.Remove(turn);
                }

                if ( messageRemoved || turnRemoved )
                {
                    OnConversationUpdated();

                    return true;
                }

                return true;
            }

            return false;
        }
    }

    #endregion

    #region Turn Management

    public void StartTurn(Guid turnId, IEnumerable<string> participantIds)
    {
        lock (_lock)
        {
            if ( _currentTurnId != Guid.Empty && _currentMessageBuffers.Count > 0 )
            {
                Debug.WriteLine($"Warning: Starting new turn {turnId} while previous turn {_currentTurnId} was pending. Aborting previous turn.");
                InternalAbortTurn();
            }

            _currentTurnId             = turnId;
            _currentTurnParticipantIds = new HashSet<string>(participantIds.Distinct());

            foreach ( var id in _currentTurnParticipantIds )
            {
                if ( !_participants.ContainsKey(id) )
                {
                    InternalAbortTurn(false);

                    throw new ArgumentException($"Participant with ID '{id}' not found in the conversation context.", nameof(participantIds));
                }
            }

            _currentMessageBuffers.Clear();
            _participantsReadyToCommit.Clear();
            _turnStartTime   = DateTimeOffset.UtcNow;
            _turnInterrupted = false;

            foreach ( var id in _currentTurnParticipantIds )
            {
                _currentMessageBuffers[id] = new StringBuilder();
            }
        }
    }

    public void AppendToTurn(string participantId, string chunk)
    {
        lock (_lock)
        {
            if ( _currentTurnId == Guid.Empty )
            {
                throw new InvalidOperationException("Cannot append to turn: No turn is currently active.");
            }

            if ( !_currentTurnParticipantIds.Contains(participantId) )
            {
                throw new InvalidOperationException($"Participant '{participantId}' is not designated as part of the current turn (ID: {_currentTurnId}).");
            }

            if ( !_currentMessageBuffers.TryGetValue(participantId, out var buffer) )
            {
                buffer                                = new StringBuilder();
                _currentMessageBuffers[participantId] = buffer;
                Debug.WriteLine($"Warning: Buffer for participant {participantId} was missing in AppendToTurn.");
            }

            buffer.Append(chunk);
            OnConversationUpdated();
        }
    }

    public string GetPendingMessageText(string participantId)
    {
        lock (_lock)
        {
            if ( _currentTurnId == Guid.Empty )
            {
                return string.Empty;
            }

            if ( _currentMessageBuffers.TryGetValue(participantId, out var buffer) )
            {
                return buffer.ToString();
            }

            return string.Empty;
        }
    }

    public void CompleteTurnPart(string participantId, bool interrupted = false)
    {
        lock (_lock)
        {
            if ( _currentTurnId == Guid.Empty )
            {
                return;
            }

            if ( !_currentTurnParticipantIds.Contains(participantId) )
            {
                return;
            }

            if ( _participantsReadyToCommit.Contains(participantId) )
            {
                return;
            }

            if ( !_currentMessageBuffers.TryGetValue(participantId, out var buffer) || buffer.Length == 0 )
            {
                Debug.WriteLine($"Debug: CompleteTurnPart called for participant {participantId} in turn {_currentTurnId} with no message content.");
            }

            if ( _participants.TryGetValue(participantId, out var participantInfo) )
            {
                var text = buffer!.ToString();
                var message = new ChatMessage(
                                              Guid.NewGuid(),
                                              participantId,
                                              participantInfo.Name,
                                              text,
                                              DateTimeOffset.UtcNow,
                                              false,
                                              participantInfo.Role
                                             );

                var turn = _history.FirstOrDefault(t => t.TurnId == _currentTurnId);
                if ( turn == null )
                {
                    turn = new InteractionTurn(
                                               _currentTurnId,
                                               _currentTurnParticipantIds,
                                               _turnStartTime,
                                               null,
                                               new List<ChatMessage> { message },
                                               interrupted
                                              );

                    _history.Add(turn);
                    ApplyCleanupStrategy();
                }
                else
                {
                    turn.Messages.Add(message);
                    turn.WasInterrupted = turn.WasInterrupted || interrupted;
                }

                _currentMessageBuffers.Remove(participantId);
            }
            else
            {
                return;
            }

            _participantsReadyToCommit.Add(participantId);
            _turnInterrupted = _turnInterrupted || interrupted;

            if ( _participantsReadyToCommit.IsSupersetOf(_currentTurnParticipantIds) )
            {
                FinalizeCurrentTurn();
            }

            OnConversationUpdated();
        }
    }

    public void AbortTurn()
    {
        lock (_lock)
        {
            InternalAbortTurn();
        }
    }

    #endregion

    #region History & Projection

    public InteractionTurn? PendingTurn
    {
        get
        {
            lock (_lock)
            {
                return CreatePendingTurnSnapshot();
            }
        }
    }

    public IReadOnlyList<InteractionTurn> GetProjectedHistory()
    {
        lock (_lock)
        {
            var projectedHistory = new List<InteractionTurn>(_history);
            var pendingTurn      = CreatePendingTurnSnapshot();
            if ( pendingTurn != null )
            {
                projectedHistory.Add(pendingTurn);
            }

            return projectedHistory.AsReadOnly();
        }
    }

    public ChatHistory GetSemanticKernelChatHistory(bool includePendingTurn = true)
    {
        lock (_lock)
        {
            var skChatHistory = new ChatHistory();

            // 1. Add System Prompt (from options)
            if ( !string.IsNullOrWhiteSpace(_options.SystemPrompt) )
            {
                skChatHistory.AddSystemMessage(_options.SystemPrompt);
            }
            else if ( !string.IsNullOrWhiteSpace(_options.SystemPromptFile) && PromptUtils.TryGetPrompt(_options.SystemPromptFile, out var systemPrompt) )
            {
                skChatHistory.AddSystemMessage(systemPrompt!);
            }

            // 2. Add Metadata Message
            var metadata           = new { topics = _options.Topics, context = _options.CurrentContext ?? string.Empty, visual_context = _currentVisualContext ?? string.Empty };
            var serializedMetadata = JsonSerializer.Serialize(metadata, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
            skChatHistory.AddUserMessage(serializedMetadata);

            // 3. Add Committed History Messages
            foreach ( var message in _history.SelectMany(turn => turn.Messages) )
            {
                AddMessageToSkHistory(skChatHistory, message);
            }

            // 4. Optionally Add Pending Turn Messages
            if ( includePendingTurn )
            {
                var pendingTurn = CreatePendingTurnSnapshot();
                if ( pendingTurn == null )
                {
                    return skChatHistory;
                }

                foreach ( var message in pendingTurn.Messages )
                {
                    AddMessageToSkHistory(skChatHistory, message);
                }
            }

            return skChatHistory;
        }
    }

    #endregion
}

using System.Text;

namespace PersonaEngine.Lib.Live2D.App;

public class WavFileInfo
{
    public int _bitsPerSample;

    public string _fileName;

    public int _numberOfChannels;

    public int _samplePerChannel;

    public int _samplingRate;
}

public class LAppWavFileHandler
{
    private readonly ByteReader _byteReader;

    private readonly WavFileInfo _wavFileInfo;

    private double _lastRms;

    private float[] _pcmData;

    private double _sampleOffset;

    private double _userTimeSeconds;

    public LAppWavFileHandler()
    {
        _pcmData         = Array.Empty<float>();
        _userTimeSeconds = 0.0;
        _lastRms         = 0.0;
        _sampleOffset    = 0.0;
        _wavFileInfo     = new WavFileInfo();
        _byteReader      = new ByteReader();
    }

    public bool Update(float deltaTimeSeconds)
    {
        double goalOffset;
        float  rms;

        // データロード前/ファイル末尾に達した場合は更新しない
        if ( _pcmData == null || _sampleOffset >= _wavFileInfo._samplePerChannel )
        {
            _lastRms = 0.0f;

            return false;
        }

        // 経過時間後の状態を保持
        _userTimeSeconds += deltaTimeSeconds;
        goalOffset       =  Math.Floor(_userTimeSeconds * _wavFileInfo._samplingRate);
        if ( goalOffset > _wavFileInfo._samplePerChannel )
        {
            goalOffset = _wavFileInfo._samplePerChannel;
        }

        // RMS計測
        rms = 0.0f;
        for ( var channelCount = 0; channelCount < _wavFileInfo._numberOfChannels; channelCount++ )
        {
            for ( var sampleCount = (int)_sampleOffset; sampleCount < goalOffset; sampleCount++ )
            {
                var index = sampleCount * _wavFileInfo._numberOfChannels + channelCount;
                if ( index >= _pcmData.Length )
                {
                    // Ensure we do not go out of bounds
                    break;
                }

                var pcm = _pcmData[index];
                rms += pcm * pcm;
            }
        }

        rms = (float)Math.Sqrt(rms / (_wavFileInfo._numberOfChannels * (goalOffset - _sampleOffset)));

        _lastRms      = rms;
        _sampleOffset = goalOffset;

        return true;
    }

    public void Start(string filePath)
    {
        // サンプル位参照位置を初期化
        _sampleOffset    = 0;
        _userTimeSeconds = 0.0f;

        // RMS値をリセット
        _lastRms = 0.0f;
    }

    public double GetRms() { return _lastRms; }

    public async Task<bool> LoadWavFile(string filePath)
    {
        if ( _pcmData != null )
        {
            ReleasePcmData();
        }

        // ファイルロード
        var response = await FetchAsync(filePath);
        if ( response != null )
        {
            // Process the response to load PCM data
            return await AsyncWavFileManager(filePath);
        }

        return false;
    }

    private async Task<byte[]> FetchAsync(string filePath) { return await Task.Run(() => File.ReadAllBytes(filePath)); }

    public async Task<bool> AsyncWavFileManager(string filePath)
    {
        var ret = false;
        _byteReader._fileByte     = await FetchAsync(filePath);
        _byteReader._fileDataView = new MemoryStream(_byteReader._fileByte);
        _byteReader._fileSize     = _byteReader._fileByte.Length;
        _byteReader._readOffset   = 0;

        // Check if file load failed or if there is not enough size for the signature "RIFF"
        if ( _byteReader._fileByte == null || _byteReader._fileSize < 4 )
        {
            return false;
        }

        // File name
        _wavFileInfo._fileName = filePath;

        try
        {
            // Signature "RIFF"
            if ( !_byteReader.GetCheckSignature("RIFF") )
            {
                ret = false;

                throw new Exception("Cannot find Signature 'RIFF'.");
            }

            // File size - 8 (skip)
            _byteReader.Get32LittleEndian();
            // Signature "WAVE"
            if ( !_byteReader.GetCheckSignature("WAVE") )
            {
                ret = false;

                throw new Exception("Cannot find Signature 'WAVE'.");
            }

            // Signature "fmt "
            if ( !_byteReader.GetCheckSignature("fmt ") )
            {
                ret = false;

                throw new Exception("Cannot find Signature 'fmt'.");
            }

            // fmt chunk size
            var fmtChunkSize = (int)_byteReader.Get32LittleEndian();
            // Format ID must be 1 (linear PCM)
            if ( _byteReader.Get16LittleEndian() != 1 )
            {
                ret = false;

                throw new Exception("File is not linear PCM.");
            }

            // Number of channels
            _wavFileInfo._numberOfChannels = (int)_byteReader.Get16LittleEndian();
            // Sampling rate
            _wavFileInfo._samplingRate = (int)_byteReader.Get32LittleEndian();
            // Data rate [byte/sec] (skip)
            _byteReader.Get32LittleEndian();
            // Block size (skip)
            _byteReader.Get16LittleEndian();
            // Bits per sample
            _wavFileInfo._bitsPerSample = (int)_byteReader.Get16LittleEndian();
            // Skip the extended part of the fmt chunk
            if ( fmtChunkSize > 16 )
            {
                _byteReader._readOffset += fmtChunkSize - 16;
            }

            // Skip until "data" chunk appears
            while ( !_byteReader.GetCheckSignature("data") && _byteReader._readOffset < _byteReader._fileSize )
            {
                _byteReader._readOffset += (int)_byteReader.Get32LittleEndian() + 4;
            }

            // "data" chunk not found in the file
            if ( _byteReader._readOffset >= _byteReader._fileSize )
            {
                ret = false;

                throw new Exception("Cannot find 'data' Chunk.");
            }

            // Number of samples
            {
                var dataChunkSize = (int)_byteReader.Get32LittleEndian();
                _wavFileInfo._samplePerChannel = dataChunkSize * 8 / (_wavFileInfo._bitsPerSample * _wavFileInfo._numberOfChannels);
            }

            // Allocate memory
            _pcmData = new float[_wavFileInfo._numberOfChannels * _wavFileInfo._samplePerChannel];
            // Retrieve waveform data
            for ( var sampleCount = 0; sampleCount < _wavFileInfo._samplePerChannel; sampleCount++ )
            {
                for ( var channelCount = 0; channelCount < _wavFileInfo._numberOfChannels; channelCount++ )
                {
                    _pcmData[sampleCount * _wavFileInfo._numberOfChannels + channelCount] = GetPcmSample();
                }
            }

            ret = true;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }

        return ret;
    }

    public float GetPcmSample()
    {
        int pcm32;

        // Expand to 32-bit width and round to the range -1 to 1
        switch ( _wavFileInfo._bitsPerSample )
        {
            case 8:
                pcm32 =   _byteReader.Get8() - 128;
                pcm32 <<= 24;

                break;
            case 16:
                pcm32 = (int)_byteReader.Get16LittleEndian() << 16;

                break;
            case 24:
                pcm32 = (int)_byteReader.Get24LittleEndian() << 8;

                break;
            default:
                // Unsupported bit width
                pcm32 = 0;

                break;
        }

        return pcm32 / 2147483647f; // float.MaxValue;
    }

    private void ReleasePcmData()
    {
        for ( var channelCount = 0; channelCount < _wavFileInfo._numberOfChannels; channelCount++ )
        {
            for ( var sampleCount = 0; sampleCount < _wavFileInfo._samplePerChannel; sampleCount++ )
            {
                _pcmData[sampleCount * _wavFileInfo._numberOfChannels + channelCount] = 0.0f;
            }
        }

        _pcmData = Array.Empty<float>();
    }
}

public class ByteReader
{
    public byte[] _fileByte;

    public MemoryStream _fileDataView;

    public int _fileSize;

    public int _readOffset;

    public int Get8()
    {
        var returnValue = _fileDataView.ReadByte();
        _readOffset++;

        return returnValue;
    }

    /**
     * @brief 16ビット読み込み（リトルエンディアン）
     * @return Csm::csmUint16 読み取った16ビット値
     */
    public uint Get16LittleEndian()
    {
        var ret =
            (uint)(Get8() << 0) |
            (uint)(Get8() << 8);

        return ret;
    }

    /// <summary>
    ///     24ビット読み込み（リトルエンディアン）
    /// </summary>
    /// <returns>読み取った24ビット値（下位24ビットに設定）</returns>
    public uint Get24LittleEndian()
    {
        var ret =
            (uint)(Get8() << 0) |
            (uint)(Get8() << 8) |
            (uint)(Get8() << 16);

        return ret;
    }

    /// <summary>
    ///     32ビット読み込み（リトルエンディアン）
    /// </summary>
    /// <returns>読み取った32ビット値</returns>
    public uint Get32LittleEndian()
    {
        var ret =
            (uint)(Get8() << 0) |
            (uint)(Get8() << 8) |
            (uint)(Get8() << 16) |
            (uint)(Get8() << 24);

        return ret;
    }

    /// <summary>
    ///     シグネチャの取得と参照文字列との一致チェック
    /// </summary>
    /// <param name="reference">検査対象のシグネチャ文字列</param>
    /// <returns>true 一致している, false 一致していない</returns>
    public bool GetCheckSignature(string reference)
    {
        if ( reference.Length != 4 )
        {
            return false;
        }

        var getSignature    = new byte[4];
        var referenceString = Encoding.UTF8.GetBytes(reference);

        for ( var signatureOffset = 0; signatureOffset < 4; signatureOffset++ )
        {
            getSignature[signatureOffset] = (byte)Get8();
        }

        return getSignature[0] == referenceString[0] &&
               getSignature[1] == referenceString[1] &&
               getSignature[2] == referenceString[2] &&
               getSignature[3] == referenceString[3];
    }
}

using System.Diagnostics;
using System.Threading.Channels;

using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.Logging;

using Polly;
using Polly.Registry;

namespace PersonaEngine.Lib.LLM;

public class SemanticKernelChatEngine : IChatEngine
{
    private readonly IChatCompletionService _chatCompletionService;

    private readonly ILogger<SemanticKernelChatEngine> _logger;

    private readonly ResiliencePipeline _resiliencePipeline;

    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public SemanticKernelChatEngine(
        Kernel                            kernel,
        ILogger<SemanticKernelChatEngine> logger, ResiliencePipelineProvider<string> resiliencePipelineProvider)
    {
        _logger                = logger ?? throw new ArgumentNullException(nameof(logger));
        _resiliencePipeline    = resiliencePipelineProvider.GetPipeline("semantickernel-chat");
        _chatCompletionService = kernel.GetRequiredService<IChatCompletionService>("text");

        _logger.LogInformation("SemanticKernelChatEngine initialized successfully");
    }

    public void Dispose()
    {
        try
        {
            _logger.LogInformation("Disposing SemanticKernelChatEngine");
            _semaphore.Dispose();
            GC.SuppressFinalize(this);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while disposing SemanticKernelChatEngine");
        }
    }

    public async Task<CompletionReason> GetStreamingChatResponseAsync(
        IConversationContext        context,
        ChannelWriter<IOutputEvent> outputWriter,
        Guid                        turnId,
        Guid                        sessionId,
        PromptExecutionSettings?    executionSettings = null,
        CancellationToken           cancellationToken = default)
    {
        await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
        var  completedReason = CompletionReason.Completed;
        var  firstChunk      = true;
        var  stopwatch       = Stopwatch.StartNew();
        long chunkCount      = 0;

        try
        {
            var history = context.GetSemanticKernelChatHistory();

            var rc = ResilienceContextPool.Shared.Get(cancellationToken);
            rc.Properties.Set(ResilienceKeys.SessionId, sessionId);
            rc.Properties.Set(ResilienceKeys.Logger, _logger);

            await _resiliencePipeline.ExecuteAsync(async (x, token) =>
                                                   {
                                                       firstChunk = true;
                                                       chunkCount = 0;
                                                       x.stopwatch.Restart();

                                                       var streamingResponse = _chatCompletionService.GetStreamingChatMessageContentsAsync(
                                                                                x.history,
                                                                                x.executionSettings,
                                                                                null,
                                                                                token);

                                                       await foreach ( var chunk in streamingResponse.ConfigureAwait(false) )
                                                       {
                                                           chunkCount++;
                                                           var content = chunk.Content ?? string.Empty;

                                                           if ( firstChunk )
                                                           {
                                                               var firstChunkEvent = new LlmStreamStartEvent(x.sessionId, x.turnId, DateTimeOffset.UtcNow);
                                                               await x.outputWriter.WriteAsync(firstChunkEvent, token).ConfigureAwait(false);
                                                               firstChunk = false;
                                                           }

                                                           var chunkEvent = new LlmChunkEvent(x.sessionId, x.turnId, DateTimeOffset.UtcNow, content);
                                                           await x.outputWriter.WriteAsync(chunkEvent, token).ConfigureAwait(false);

                                                           _logger.LogTrace("Received chunk {ChunkNumber}: {ChunkLength} characters for TurnId: {TurnId}", chunkCount, content.Length, x.turnId);
                                                       }

                                                       _logger.LogInformation("Chat response streaming completed. Total chunks: {ChunkCount}, Total time: {ElapsedMs}ms", chunkCount, stopwatch.ElapsedMilliseconds);
                                                   }, (stopwatch, outputWriter, history, executionSettings, sessionId, turnId), cancellationToken).ConfigureAwait(false);

            ResilienceContextPool.Shared.Return(rc);

            stopwatch.Stop();

            if ( !firstChunk )
            {
                _logger.LogInformation(
                                       "Chat response streaming completed successfully for TurnId: {TurnId}. Total chunks: {ChunkCount}, Total time: {ElapsedMs}ms",
                                       turnId, chunkCount, stopwatch.ElapsedMilliseconds);
            }
            else
            {
                _logger.LogWarning("Chat response streaming finished for TurnId: {TurnId}, but no chunks were received (potentially empty response or immediate failure). Time: {ElapsedMs}ms", turnId, stopwatch.ElapsedMilliseconds);
            }
        }
        catch (OperationCanceledException)
        {
            completedReason = CompletionReason.Cancelled;
            stopwatch.Stop();
        }
        catch (Exception ex)
        {
            completedReason = CompletionReason.Error;
            stopwatch.Stop();
            await outputWriter.WriteAsync(new ErrorOutputEvent(sessionId, turnId, DateTimeOffset.UtcNow, ex), cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            if ( !firstChunk )
            {
                var endEvent = new LlmStreamEndEvent(sessionId, turnId, DateTimeOffset.UtcNow, completedReason);
                try
                {
                    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                    await outputWriter.WriteAsync(endEvent, cts.Token).ConfigureAwait(false);
                }
                catch (Exception finalEx)
                {
                    _logger.LogError(finalEx, "Failed to write LlmStreamEndEvent for TurnId: {TurnId}", turnId);
                }
            }

            _semaphore.Release();
        }

        return completedReason;
    }
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents an audio source that concatenates multiple audio sources
/// </summary>
public sealed class ConcatAudioSource : IAudioSource
{
    private readonly IAudioSource[] sources;

    public ConcatAudioSource(IAudioSource[] sources, IReadOnlyDictionary<string, string> metadata)
    {
        this.sources = sources;
        Metadata     = metadata;
        if ( sources.Length == 0 )
        {
            throw new ArgumentException("At least one source must be provided.", nameof(sources));
        }

        var duration      = TimeSpan.Zero;
        var sampleRate    = sources[0].SampleRate;
        var framesCount   = 0L;
        var channelCount  = sources[0].ChannelCount;
        var bitsPerSample = sources[0].BitsPerSample;
        var totalDuration = TimeSpan.Zero;

        for ( var i = 0; i < sources.Length; i++ )
        {
            duration      += sources[i].Duration;
            totalDuration += sources[i].TotalDuration;
            framesCount   += sources[i].FramesCount;
            if ( channelCount != sources[i].ChannelCount )
            {
                throw new ArgumentException("All sources must have the same channel count.", nameof(sources));
            }

            if ( sampleRate != sources[i].SampleRate )
            {
                throw new ArgumentException("All sources must have the same sample rate.", nameof(sources));
            }

            if ( bitsPerSample != sources[i].BitsPerSample )
            {
                throw new ArgumentException("All sources must have the same bits per sample.", nameof(sources));
            }
        }

        Duration      = duration;
        SampleRate    = sampleRate;
        FramesCount   = framesCount;
        ChannelCount  = channelCount;
        BitsPerSample = bitsPerSample;
        TotalDuration = totalDuration;
    }

    public IReadOnlyDictionary<string, string> Metadata { get; }

    public TimeSpan Duration { get; }

    public TimeSpan TotalDuration { get; }

    public uint SampleRate { get; }

    public long FramesCount { get; }

    public ushort ChannelCount { get; }

    public bool IsInitialized => true;

    public ushort BitsPerSample { get; }

    public void Dispose()
    {
        foreach ( var source in sources )
        {
            source.Dispose();
        }
    }

    public async Task<Memory<byte>> GetFramesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var slices = await GetMemorySlicesAsync(
                                                (source, offset, frames, token) => source.GetFramesAsync(offset, frames, token),
                                                startFrame,
                                                maxFrames,
                                                cancellationToken);

        return MergeMemorySlices(slices);
    }

    public async Task<Memory<float>> GetSamplesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var slices = await GetMemorySlicesAsync(
                                                (source, offset, frames, token) => source.GetSamplesAsync(offset, frames, token),
                                                startFrame,
                                                maxFrames,
                                                cancellationToken);

        return MergeMemorySlices(slices);
    }

    public async Task<int> CopyFramesAsync(Memory<byte> destination, long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var slices = await GetMemorySlicesAsync(
                                                (source, offset, frames, token) => source.GetFramesAsync(offset, frames, token),
                                                startFrame,
                                                maxFrames,
                                                cancellationToken);

        CopySlices(slices, destination);
        long length = slices.Sum(slice => slice.Length);

        return (int)(length / BitsPerSample * 8 / ChannelCount);
    }

    private async Task<List<Memory<T>>> GetMemorySlicesAsync<T>(Func<IAudioSource, long, int, CancellationToken, Task<Memory<T>>> getMemoryFunc,
                                                                long                                                              startFrame,
                                                                int                                                               maxFrames,
                                                                CancellationToken                                                 cancellationToken)
    {
        var result       = new List<Memory<T>>();
        var framesToRead = maxFrames;
        var offset       = startFrame;

        foreach ( var source in sources )
        {
            if ( offset >= source.FramesCount )
            {
                offset -= source.FramesCount;

                continue;
            }

            var framesFromThisSource = Math.Min(framesToRead, (int)(source.FramesCount - offset));
            var memory               = await getMemoryFunc(source, offset, framesFromThisSource, cancellationToken);

            result.Add(memory);

            framesToRead -= framesFromThisSource;
            offset       =  0;

            if ( framesToRead <= 0 )
            {
                break;
            }
        }

        return result;
    }

    private static Memory<T> MergeMemorySlices<T>(List<Memory<T>> slices)
    {
        if ( slices.Count == 1 )
        {
            return slices[0]; // If all frames/samples are from a single source, return directly
        }

        var totalSize = slices.Sum(slice => slice.Length);
        var merged    = new T[totalSize];
        CopySlices(slices, merged);

        return merged.AsMemory();
    }

    private static void CopySlices<T>(List<Memory<T>> slices, Memory<T> destination)
    {
        var position = 0;

        foreach ( var slice in slices )
        {
            slice.CopyTo(destination.Slice(position));
            position += slice.Length;
        }
    }
}

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.Live2D.App;
using PersonaEngine.Lib.Live2D.Framework.Motion;

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
///     Manages Live2D model animations and expressions synchronized with detected speech emotions from audio playback.
/// </summary>
/// <remarks>
///     Responsibilities include:
///     - Mapping detected emotion events (e.g., emojis from <see cref="IEmotionService" />) to Live2D expressions and
///     motions.
///     - Applying expressions for a configurable duration (<see cref="EXPRESSION_HOLD_DURATION_SECONDS" />) before
///     potentially reverting to neutral.
///     - Playing specific emotion-driven motions or a neutral "talking" motion during audio playback.
///     - Handling emotion timing based on the audio player's current position.
///     - Persisting the last triggered expression/motion until the hold time expires or it's replaced.
///     This service assumes LipSync, Blinking, and Idle animations are managed by other services with appropriate
///     priorities.
///     It requires an <see cref="IEmotionService" /> for timed emotion data and an
///     <see cref="IAudioProgressNotifier" /> for playback state and time.
/// </remarks>
public class EmotionAnimationService : ILive2DAnimationService
{
    public EmotionAnimationService(ILogger<EmotionAnimationService> logger, IAudioProgressNotifier audioProgressNotifier, IEmotionService emotionService)
    {
        _logger                = logger;
        _audioProgressNotifier = audioProgressNotifier;
        _emotionService        = emotionService;

        SubscribeToAudioProgressNotifier();
    }

    #region Nested Types

    private record EmotionMapping(string? ExpressionId, string? MotionGroup);

    #endregion

    #region Configuration

    // How long to hold an expression after it's triggered before reverting to neutral (if playback stops or no new emotion overrides it).
    private const float EXPRESSION_HOLD_DURATION_SECONDS = 3.0f;

    // Expression ID for the neutral state.
    private const string NEUTRAL_EXPRESSION_ID = "neutral";

    // Motion group used for the default talking animation when no specific emotion motion is active.
    private const string NEUTRAL_TALKING_MOTION_GROUP = "Talking";

    // Priority for motions triggered by specific emotions. Should typically be high to override idle/neutral talking.
    private const MotionPriority EMOTION_MOTION_PRIORITY = MotionPriority.PriorityForce;

    // Priority for the neutral talking motion. Should be lower than emotion motions but higher than idle.
    private const MotionPriority NEUTRAL_TALKING_MOTION_PRIORITY = MotionPriority.PriorityNormal;

    // Defines the mapping between detected emotion identifiers (e.g., emojis) and Live2D assets.
    // TODO: Consider loading this from an external configuration file for customization.
    private static readonly Dictionary<string, EmotionMapping> EmotionMap = new() {
                                                                                // Positive Emotions
                                                                                { "😊", new EmotionMapping("happy", "Happy") },
                                                                                { "🤩", new EmotionMapping("excited_star", "Excited") },
                                                                                { "😎", new EmotionMapping("cool", "Confident") },
                                                                                { "😏", new EmotionMapping("smug", "Confident") },
                                                                                { "💪", new EmotionMapping("determined", "Confident") },
                                                                                // Reactive Emotions
                                                                                { "😳", new EmotionMapping("embarrassed", "Nervous") },
                                                                                { "😲", new EmotionMapping("shocked", "Surprised") },
                                                                                { "🤔", new EmotionMapping("thinking", "Thinking") },
                                                                                { "👀", new EmotionMapping("suspicious", "Thinking") },
                                                                                // Negative Emotions
                                                                                { "😤", new EmotionMapping("frustrated", "Angry") },
                                                                                { "😢", new EmotionMapping("sad", "Sad") },
                                                                                { "😅", new EmotionMapping("awkward", "Nervous") },
                                                                                { "🙄", new EmotionMapping("dismissive", "Annoyed") },
                                                                                // Expressive Reactions
                                                                                { "💕", new EmotionMapping("adoring", "Happy") },
                                                                                { "😂", new EmotionMapping("laughing", "Happy") },
                                                                                { "🔥", new EmotionMapping("passionate", "Excited") },
                                                                                { "✨", new EmotionMapping("sparkle", "Happy") }
                                                                                // Neutral (can map if specific neutral actions are needed, otherwise handled implicitly)
                                                                                // { "😐", new EmotionMapping(NEUTRAL_EXPRESSION_ID, null) } // Example if needed
                                                                                };

    #endregion

    #region Dependencies

    private readonly ILogger<EmotionAnimationService> _logger;

    private readonly IAudioProgressNotifier _audioProgressNotifier;

    private readonly IEmotionService _emotionService;

    #endregion

    #region State

    private LAppModel? _model;

    private readonly List<EmotionTiming> _activeEmotions = new();

    private readonly HashSet<string> _availableExpressions = new();

    private readonly HashSet<string> _availableMotionGroups = new();

    private int _currentEmotionIndex = -1;

    private string? _triggeredEmotionEmoji = null;

    private string? _activeExpressionId = null;

    private float _timeSinceExpressionSet = 0.0f;

    private CubismMotionQueueEntry? _currentEmotionMotionEntry = null;

    private CubismMotionQueueEntry? _neutralTalkingMotionEntry = null;

    private bool _isStarted = false;

    private bool _isPlaying = false;

    private bool _disposed = false;

    #endregion

    #region ILive2DAnimationService Implementation

    private void SubscribeToAudioProgressNotifier()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        UnsubscribeFromCurrentNotifier();

        _audioProgressNotifier.ChunkPlaybackStarted += HandleChunkStarted;
        _audioProgressNotifier.ChunkPlaybackEnded   += HandleChunkEnded;
        _audioProgressNotifier.PlaybackProgress     += HandleProgress;
    }

    private void UnsubscribeFromCurrentNotifier()
    {
        _audioProgressNotifier.ChunkPlaybackStarted -= HandleChunkStarted;
        _audioProgressNotifier.ChunkPlaybackEnded   -= HandleChunkEnded;
        _audioProgressNotifier.PlaybackProgress     -= HandleProgress;
    }

    public void Start(LAppModel model)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        _model = model;

        ValidateModelAssets();

        _isStarted = true;
        ResetEmotionState(true);
        _logger.LogInformation("EmotionAnimationService started for model.");
    }

    public void Stop()
    {
        _isStarted = false;
        ResetEmotionState(true);
        _logger.LogInformation("EmotionAnimationService stopped.");
    }

    #endregion

    #region Update Logic

    public void Update(float deltaTime)
    {
        if ( deltaTime <= 0.0f || _disposed || !_isStarted || _model == null )
        {
            return;
        }

        _timeSinceExpressionSet += deltaTime;
        CheckExpressionTimeout();

        if ( _isPlaying )
        {
            ManageNeutralTalkingMotion();
        }
        // Optionally ensure neutral talking motion is stopped/faded out if needed when not playing,
        // though priority system might handle this.
        // If _neutralTalkingMotionEntry exists and isn't finished, it might need explicit stopping
        // depending on desired behavior when audio stops mid-talk.
        // For now, we assume idle animations (handled elsewhere) will override it.
    }

    private void CheckExpressionTimeout()
    {
        if ( _activeExpressionId == null || _activeExpressionId == NEUTRAL_EXPRESSION_ID ||
             !(_timeSinceExpressionSet >= EXPRESSION_HOLD_DURATION_SECONDS) )
        {
            return;
        }

        _logger.LogTrace("Expression '{ExpressionId}' hold time expired ({HoldDuration}s). Reverting to neutral.",
                         _activeExpressionId, EXPRESSION_HOLD_DURATION_SECONDS);

        ApplyNeutralExpression();
    }

    private void ManageNeutralTalkingMotion()
    {
        if ( _model == null )
        {
            return;
        }

        var isEmotionMotionActive = _currentEmotionMotionEntry is { Finished: false };

        if ( isEmotionMotionActive )
        {
            if ( _currentEmotionMotionEntry is { Finished: true } )
            {
                _currentEmotionMotionEntry = null;
            }

            return;
        }

        if ( _currentEmotionMotionEntry?.Finished ?? false )
        {
            _currentEmotionMotionEntry = null;
        }

        var shouldStartNeutralTalk = _availableMotionGroups.Contains(NEUTRAL_TALKING_MOTION_GROUP) &&
                                     (_neutralTalkingMotionEntry == null || _neutralTalkingMotionEntry.Finished);

        if ( !shouldStartNeutralTalk )
        {
            return;
        }

        _logger.LogTrace("Starting neutral talking motion (group: {MotionGroup}).", NEUTRAL_TALKING_MOTION_GROUP);
        _neutralTalkingMotionEntry = _model.StartRandomMotion(NEUTRAL_TALKING_MOTION_GROUP, NEUTRAL_TALKING_MOTION_PRIORITY);
        if ( _neutralTalkingMotionEntry == null )
        {
            _logger.LogDebug("Could not start neutral talking motion for group '{MotionGroup}'.", NEUTRAL_TALKING_MOTION_GROUP);
        }
    }

    private void UpdateEmotionBasedOnTime(float currentTime)
    {
        if ( _activeEmotions.Count == 0 || !_isPlaying )
        {
            return;
        }

        var targetEmotionIndex = -1;
        for ( var i = 0; i < _activeEmotions.Count; i++ )
        {
            if ( _activeEmotions[i].Timestamp <= currentTime )
            {
                targetEmotionIndex = i;
            }
            else
            {
                break;
            }
        }

        if ( targetEmotionIndex != -1 && targetEmotionIndex != _currentEmotionIndex )
        {
            _logger.LogTrace("Emotion change detected at T={CurrentTime:F3}. New index: {NewIndex}, Old index: {OldIndex}",
                             currentTime, targetEmotionIndex, _currentEmotionIndex);

            _currentEmotionIndex = targetEmotionIndex;
            var newEmotionEmoji = _activeEmotions[_currentEmotionIndex].Emotion;
            ApplyEmotion(newEmotionEmoji);
        }
        // If targetEmotionIndex is -1 (currentTime is before the first emotion),
        // or if the index hasn't changed, do nothing. The state persists.
    }

    private void ApplyEmotion(string? emotionEmoji)
    {
        if ( !_isStarted || _model == null )
        {
            return;
        }

        if ( emotionEmoji == _triggeredEmotionEmoji )
        {
            return;
        }

        _logger.LogDebug("Applying triggered emotion: {Emotion}", emotionEmoji ?? "Neutral (explicit)");
        _triggeredEmotionEmoji = emotionEmoji;

        EmotionMapping? mapping      = null;
        var             foundMapping = emotionEmoji != null && EmotionMap.TryGetValue(emotionEmoji, out mapping);

        var targetExpressionId = foundMapping ? mapping?.ExpressionId : NEUTRAL_EXPRESSION_ID;
        SetExpression(targetExpressionId);

        if ( _currentEmotionMotionEntry?.Finished ?? false )
        {
            _currentEmotionMotionEntry = null;
        }

        var targetMotionGroup = foundMapping ? mapping?.MotionGroup : null;

        if ( !string.IsNullOrEmpty(targetMotionGroup) )
        {
            if ( _availableMotionGroups.Contains(targetMotionGroup) )
            {
                _logger.LogTrace("Attempting to start emotion motion from group: {MotionGroup} with priority {Priority}.",
                                 targetMotionGroup, EMOTION_MOTION_PRIORITY);

                var newMotionEntry = _model.StartRandomMotion(targetMotionGroup, EMOTION_MOTION_PRIORITY);
                if ( newMotionEntry != null )
                {
                    _currentEmotionMotionEntry = newMotionEntry;
                    _neutralTalkingMotionEntry = null;
                    _logger.LogTrace("Emotion motion started successfully.");
                }
                else
                {
                    _logger.LogWarning("Could not start motion for group '{MotionGroup}'. Is the group empty or definition invalid?", targetMotionGroup);
                    _currentEmotionMotionEntry = null;
                }
            }
            else
            {
                _logger.LogWarning("Motion group '{MotionGroup}' for emotion '{Emotion}' not found in the current model.", targetMotionGroup, emotionEmoji);
                _currentEmotionMotionEntry = null;
            }
        }
        else
        {
            _currentEmotionMotionEntry = null;
            _logger.LogTrace("No specific motion group mapped for emotion '{Emotion}'.", emotionEmoji ?? "Neutral");
        }
    }

    private void SetExpression(string? expressionId)
    {
        if ( !_isStarted || _model == null )
        {
            return;
        }

        var actualExpressionId = string.IsNullOrEmpty(expressionId) ? NEUTRAL_EXPRESSION_ID : expressionId;

        if ( string.IsNullOrEmpty(NEUTRAL_EXPRESSION_ID) && string.IsNullOrEmpty(expressionId) )
        {
            actualExpressionId = null;
        }

        if ( actualExpressionId == _activeExpressionId )
        {
            return;
        }

        var isTargetNeutral = string.IsNullOrEmpty(actualExpressionId) || actualExpressionId == NEUTRAL_EXPRESSION_ID;

        if ( isTargetNeutral )
        {
            ApplyNeutralExpression();
        }
        else if ( _availableExpressions.Contains(actualExpressionId!) )
        {
            _logger.LogTrace("Setting expression: {ExpressionId}", actualExpressionId);
            _model.SetExpression(actualExpressionId!);
            _activeExpressionId     = actualExpressionId;
            _timeSinceExpressionSet = 0.0f;
        }
        else
        {
            _logger.LogWarning("Expression '{ExpressionId}' not found in model. Applying neutral instead.", actualExpressionId);
            ApplyNeutralExpression(); // Fallback to neutral
        }
    }

    private void ApplyNeutralExpression()
    {
        if ( !_isStarted || _model == null )
        {
            return;
        }

        var neutralToApply = string.IsNullOrEmpty(NEUTRAL_EXPRESSION_ID) ? null : NEUTRAL_EXPRESSION_ID;

        if ( _activeExpressionId != neutralToApply )
        {
            if ( neutralToApply != null || _activeExpressionId != null ) // Avoid logging null -> null
            {
                _logger.LogTrace("Setting neutral expression (was: {PreviousExpression}).", _activeExpressionId ?? "model default");
            }

            if ( neutralToApply != null )
            {
                if ( _availableExpressions.Contains(neutralToApply) )
                {
                    _model.SetExpression(neutralToApply);
                }
                else
                {
                    _logger.LogError("Configured NEUTRAL_EXPRESSION_ID '{NeutralId}' is not available in the model, cannot apply.", NEUTRAL_EXPRESSION_ID);
                    neutralToApply = null;
                }
            }
            else
            {
                _logger.LogTrace("No neutral expression configured. Model may revert to default.");
            }

            _activeExpressionId = neutralToApply;
        }

        _timeSinceExpressionSet = 0.0f;
    }

    #endregion

    #region Event Handlers

    private void HandleChunkStarted(object? sender, AudioChunkPlaybackStartedEvent e)
    {
        if ( !_isStarted || _model == null )
        {
            return;
        }

        _logger.LogTrace("Audio Chunk Playback Started for segment {SegmentId}.", e.Chunk.Id);
        _isPlaying = true;
        ResetEmotionState(false);

        try
        {
            var emotions = e.Chunk.GetEmotions(_emotionService);

            if ( emotions.Any() )
            {
                _activeEmotions.AddRange(emotions.OrderBy(em => em.Timestamp));
                _logger.LogDebug("Loaded {Count} timed emotions for the segment.", _activeEmotions.Count);

                UpdateEmotionBasedOnTime(0.0f);
            }
            else
            {
                _logger.LogDebug("No timed emotions found for segment {SegmentId}.", e.Chunk.Id);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving or processing emotions for audio segment {SegmentId}.", e.Chunk.Id);
        }
    }

    private void HandleChunkEnded(object? sender, AudioChunkPlaybackEndedEvent e)
    {
        if ( !_isStarted )
        {
            return;
        }

        _logger.LogTrace("Audio Chunk Playback Ended.");
        _isPlaying = false;
        _activeEmotions.Clear();
        _currentEmotionIndex = -1;

        // Expression timeout is handled by Update loop (CheckExpressionTimeout).
        // Motions will finish naturally or be overridden by idle animations (assumed).
        // Resetting _triggeredEmotionEmoji ensures the next chunk starts fresh.
        // Keep _activeExpressionId as is until timeout or next emotion.
        // _triggeredEmotionEmoji = null; // Reset last triggered emoji immediately? Or let timeout handle expression? Let timeout handle.
    }

    private void HandleProgress(object? sender, AudioPlaybackProgressEvent e)
    {
        if ( !_isStarted || !_isPlaying || _model == null )
        {
            return;
        }

        UpdateEmotionBasedOnTime((float)e.CurrentPlaybackTime.TotalSeconds);
    }

    #endregion

    #region State Management & Validation

    private void ResetEmotionState(bool forceNeutral)
    {
        _activeEmotions.Clear();
        _currentEmotionIndex = -1;

        if ( forceNeutral )
        {
            _currentEmotionMotionEntry = null;
        }

        if ( forceNeutral )
        {
            _neutralTalkingMotionEntry = null;
        }

        if ( forceNeutral )
        {
            _logger.LogTrace("Forcing neutral state on reset.");
            ApplyNeutralExpression();
            _triggeredEmotionEmoji = null;
        }

        _logger.LogTrace("Emotion state reset (Active emotions cleared. Forced neutral: {ForceNeutral}).", forceNeutral);
    }

    private void ValidateModelAssets()
    {
        if ( _model == null )
        {
            return;
        }

        _logger.LogDebug("Validating configured emotion mappings against model assets...");

        _availableExpressions.Clear();
        _availableMotionGroups.Clear();

        var modelExpressions = new HashSet<string>(_model.Expressions ?? Enumerable.Empty<string>());
        _logger.LogTrace("Model contains {Count} expressions.", modelExpressions.Count);

        var modelMotionGroups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach ( var motionKey in _model.Motions ?? Enumerable.Empty<string>() )
        {
            var groupName = LAppModel.GetMotionGroupName(motionKey); // Use static helper
            if ( !string.IsNullOrEmpty(groupName) )
            {
                modelMotionGroups.Add(groupName);
            }
        }

        _logger.LogTrace("Model contains {Count} unique motion groups (e.g., {Examples}).",
                         modelMotionGroups.Count, string.Join(", ", modelMotionGroups.Take(5)) + (modelMotionGroups.Count > 5 ? "..." : ""));

        foreach ( var (emotion, mapping) in EmotionMap )
        {
            if ( !string.IsNullOrEmpty(mapping.ExpressionId) )
            {
                if ( modelExpressions.Contains(mapping.ExpressionId) )
                {
                    _availableExpressions.Add(mapping.ExpressionId);
                }
                else
                {
                    _logger.LogWarning("Validation: Expression '{ExpressionId}' (for emotion '{Emotion}') not found.", mapping.ExpressionId, emotion);
                }
            }

            if ( string.IsNullOrEmpty(mapping.MotionGroup) )
            {
                continue;
            }

            if ( modelMotionGroups.Contains(mapping.MotionGroup) )
            {
                _availableMotionGroups.Add(mapping.MotionGroup);
            }
            else
            {
                _logger.LogWarning("Validation: Motion group '{MotionGroup}' (for emotion '{Emotion}') not found.", mapping.MotionGroup, emotion);
            }
        }

        if ( !string.IsNullOrEmpty(NEUTRAL_EXPRESSION_ID) )
        {
            if ( modelExpressions.Contains(NEUTRAL_EXPRESSION_ID) )
            {
                _availableExpressions.Add(NEUTRAL_EXPRESSION_ID);
            }
            else
            {
                _logger.LogWarning("Configured NEUTRAL_EXPRESSION_ID ('{NeutralId}') not found!", NEUTRAL_EXPRESSION_ID);
            }
        }

        if ( !string.IsNullOrEmpty(NEUTRAL_TALKING_MOTION_GROUP) )
        {
            if ( modelMotionGroups.Contains(NEUTRAL_TALKING_MOTION_GROUP) )
            {
                _availableMotionGroups.Add(NEUTRAL_TALKING_MOTION_GROUP);
            }
            else
            {
                _logger.LogWarning("Configured NEUTRAL_TALKING_MOTION_GROUP ('{Group}') not found.", NEUTRAL_TALKING_MOTION_GROUP);
            }
        }

        _logger.LogDebug("Validation complete. Usable Expressions: {ExprCount}, Usable Motion Groups: {MotionCount}",
                         _availableExpressions.Count, _availableMotionGroups.Count);
    }

    #endregion

    #region IDisposable Implementation

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if ( !_disposed )
        {
            if ( disposing )
            {
                _logger.LogDebug("Disposing EmotionAnimationService...");
                Stop();
                UnsubscribeFromCurrentNotifier();
                _activeEmotions.Clear();
                _availableExpressions.Clear();
                _availableMotionGroups.Clear();
                _model = null;
                _logger.LogInformation("EmotionAnimationService disposed.");
            }

            _disposed = true;
        }
    }

    ~EmotionAnimationService() { Dispose(false); }

    #endregion
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents an audio source that stores audio samples in memory.
/// </summary>
/// <remarks>
///     It can store samples as floats or as bytes, or both. By default, it stores samples as floats.
///     Based on your usage, you can choose to store samples as bytes, floats, or both.
///     If storing them as floats, they will be deserialized from bytes when they are added and returned directly when
///     requested.
///     If storing them as bytes, they will be serialized from floats when they are added and returned directly when
///     requested.
///     If you want to optimize your memory usage, you can store them in the same format as they are added.
///     If you want to optimize your CPU usage, you can store them in the format you want to use them in.
/// </remarks>
public class BufferedMemoryAudioSource : IAudioSource, IMemoryBackedAudioSource
{
    public const int DefaultInitialSize = 1024 * 16;

    private readonly IChannelAggregationStrategy? aggregationStrategy;

    protected byte[]? ByteFrames;

    protected float[]? FloatFrames;

    private long framesCount;

    private bool isDisposed;

    /// <summary>
    ///     Initializes a new instance of the <see cref="BufferedMemoryAudioSource" /> class.
    /// </summary>
    /// <param name="storeSamples"> A value indicating whether to store samples as floats. Default true.</param>
    /// <param name="storeBytes"> A value indicating whether to store samples as byte[]. Default false.</param>
    /// <param name="aggregationStrategy"> Optional. The channel aggregation strategy to use.</param>
    public BufferedMemoryAudioSource(IReadOnlyDictionary<string, string> metadata,
                                     bool                                storeFloats         = true,
                                     bool                                storeBytes          = false,
                                     int                                 initialSizeFloats   = DefaultInitialSize,
                                     int                                 initialSizeBytes    = DefaultInitialSize,
                                     IChannelAggregationStrategy?        aggregationStrategy = null)
    {
        Metadata                 = metadata;
        this.aggregationStrategy = aggregationStrategy;

        if ( !storeFloats && !storeBytes )
        {
            throw new ArgumentException("At least one of storeFloats or storeBytes must be true.");
        }

        if ( storeFloats )
        {
            FloatFrames = new float[initialSizeFloats];
        }

        if ( storeBytes )
        {
            ByteFrames = new byte[initialSizeBytes];
        }
    }

    /// <summary>
    ///     Gets the header of the audio source.
    /// </summary>
    protected AudioSourceHeader Header { get; private set; } = null!;

    /// <summary>
    ///     Gets the size of a single frame in the current wave file.
    /// </summary>
    public int FrameSize => BitsPerSample * ChannelCount / 8;

    /// <summary>
    ///     Represents the actual number of channels in the source.
    /// </summary>
    /// <remarks>
    ///     Note, that the actual number of channels may be different from the number of channels in the header if the source
    ///     uses an aggregation strategy.
    /// </remarks>
    public ushort ChannelCount => aggregationStrategy == null ? Header.Channels : (ushort)1;

    /// <summary>
    ///     Gets the number of samples for each channel.
    /// </summary>
    public virtual long FramesCount => framesCount;

    /// <summary>
    ///     Gets a value indicating whether the source is initialized.
    /// </summary>
    public bool IsInitialized { get; private set; }

    public uint SampleRate => Header.SampleRate;

    public ushort BitsPerSample => Header.BitsPerSample;

    public IReadOnlyDictionary<string, string> Metadata { get; }

    public virtual TimeSpan Duration => TimeSpan.FromMilliseconds(FramesCount * 1000d / SampleRate);

    public virtual TimeSpan TotalDuration => TimeSpan.FromMilliseconds(framesCount * 1000d / SampleRate);

    /// <inheritdoc />
    public void Dispose()
    {
        if ( isDisposed )
        {
            return;
        }

        Dispose(true);
        GC.SuppressFinalize(this);
        isDisposed = true;
    }

    public virtual Task<Memory<float>> GetSamplesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        ThrowIfNotInitialized();

        // We first check if we have the samples as floats, if not we deserialize them from bytes
        if ( FloatFrames != null )
        {
            return Task.FromResult(GetFloatFramesSlice(startFrame, maxFrames));
        }

        var byteSlice = GetByteFramesSlice(startFrame, maxFrames);

        return Task.FromResult(SampleSerializer.Deserialize(byteSlice, BitsPerSample));
    }

    public virtual Task<Memory<byte>> GetFramesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        // We first check if we have the samples as bytes, if not we serialize them from floats
        if ( ByteFrames != null )
        {
            return Task.FromResult(GetByteFramesSlice(startFrame, maxFrames));
        }

        var slice = GetFloatFramesSlice(startFrame, maxFrames);

        return Task.FromResult(SampleSerializer.Serialize(slice, BitsPerSample));
    }

    public virtual Task<int> CopyFramesAsync(Memory<byte> destination, long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        if ( ByteFrames != null )
        {
            var slice = GetByteFramesSlice(startFrame, maxFrames);

            slice.CopyTo(destination);
            var byteFrameCount = slice.Length / FrameSize;

            return Task.FromResult(byteFrameCount);
        }

        var floatSlice = GetFloatFramesSlice(startFrame, maxFrames);
        SampleSerializer.Serialize(floatSlice, destination, BitsPerSample);

        var frameCount = floatSlice.Length / ChannelCount;

        return Task.FromResult(frameCount);
    }

    public bool StoresFloats => FloatFrames != null;

    public bool StoresBytes => ByteFrames != null;

    ~BufferedMemoryAudioSource() { Dispose(false); }

    /// <summary>
    ///     Initializes the source with the specified header.
    /// </summary>
    /// <param name="header">The audio source header.</param>
    /// <exception cref="InvalidOperationException">Thrown when the source is already initialized.</exception>
    public virtual void Initialize(AudioSourceHeader header)
    {
        if ( IsInitialized )
        {
            throw new InvalidOperationException("The source is already initialized.");
        }

        Header        = header;
        IsInitialized = true;
    }

    /// <summary>
    ///     Adds frames to the source.
    /// </summary>
    /// <param name="frame">The frame of samples to add.</param>
    /// <exception cref="InvalidOperationException">Thrown when the source is not initialized.</exception>
    /// <exception cref="ArgumentException">Thrown when the frame size does not match the channels.</exception>
    public virtual void AddFrame(ReadOnlyMemory<float> frame)
    {
        ThrowIfNotInitialized();

        if ( frame.Length != Header.Channels )
        {
            throw new ArgumentException("The frame size does not match the channels.", nameof(frame));
        }

        if ( FloatFrames != null )
        {
            AddFrameToSamples(frame);
        }

        if ( ByteFrames != null )
        {
            AddFrameToFrames(frame);
        }

        framesCount++;
    }

    /// <summary>
    ///     Adds frames to the source.
    /// </summary>
    /// <param name="frame">The frame buffer to add.</param>
    /// <exception cref="InvalidOperationException">Thrown when the source is not initialized.</exception>
    /// <exception cref="ArgumentException">Thrown when the frame size does not match the channels.</exception>
    public virtual void AddFrame(ReadOnlyMemory<byte> frame)
    {
        ThrowIfNotInitialized();
        if ( frame.Length != Header.Channels * Header.BitsPerSample / 8 )
        {
            throw new ArgumentException("The frame size does not match the channels.", nameof(frame));
        }

        if ( FloatFrames != null )
        {
            AddFrameToSamples(frame);
        }

        if ( ByteFrames != null )
        {
            AddFrameToFrames(frame);
        }

        framesCount++;
    }

    protected void ThrowIfNotInitialized()
    {
        if ( !IsInitialized )
        {
            throw new InvalidOperationException("The source is not initialized.");
        }
    }

    /// <summary>
    ///     Disposes the object.
    /// </summary>
    /// <param name="disposing">A value indicating whether the method is called from Dispose.</param>
    protected virtual void Dispose(bool disposing)
    {
        if ( disposing )
        {
            FloatFrames = [];
            ByteFrames  = [];
        }
    }

    private Memory<float> GetFloatFramesSlice(long startFrame, int maxFrames)
    {
        var startSample = (int)(startFrame * ChannelCount);
        var length      = (int)(Math.Min(maxFrames, FramesCount - startFrame) * ChannelCount);

        return FloatFrames.AsMemory(startSample, length);
    }

    private Memory<byte> GetByteFramesSlice(long startFrame, int maxFrames)
    {
        var startByte   = (int)(startFrame * FrameSize);
        var lengthBytes = (int)(Math.Min(maxFrames, FramesCount - startFrame) * FrameSize);

        return ByteFrames.AsMemory(startByte, lengthBytes);
    }

    private void AddFrameToFrames(ReadOnlyMemory<byte> frame)
    {
        if ( ByteFrames!.Length <= FramesCount * FrameSize )
        {
            Array.Resize(ref ByteFrames, ByteFrames.Length * 2);
        }

        var startByte         = (int)(FramesCount * FrameSize);
        var destinationMemory = ByteFrames.AsMemory(startByte);
        if ( aggregationStrategy != null )
        {
            aggregationStrategy.Aggregate(frame, destinationMemory, BitsPerSample);
        }
        else
        {
            frame.Span.CopyTo(destinationMemory.Span);
        }
    }

    private void AddFrameToSamples(ReadOnlyMemory<byte> frame)
    {
        if ( FloatFrames!.Length <= FramesCount * ChannelCount )
        {
            Array.Resize(ref FloatFrames, FloatFrames.Length * 2);
        }

        var destinationMemory = FloatFrames.AsMemory((int)(FramesCount * ChannelCount));

        if ( aggregationStrategy != null )
        {
            aggregationStrategy.Aggregate(frame, destinationMemory, BitsPerSample);
        }
        else
        {
            SampleSerializer.Deserialize(frame, FloatFrames.AsMemory((int)(FramesCount * ChannelCount)), BitsPerSample);
        }
    }

    private void AddFrameToFrames(ReadOnlyMemory<float> frame)
    {
        if ( ByteFrames!.Length <= FramesCount * FrameSize )
        {
            Array.Resize(ref ByteFrames, ByteFrames.Length * 2);
        }

        var startByte         = (int)(FramesCount * FrameSize);
        var destinationMemory = ByteFrames.AsMemory(startByte);
        if ( aggregationStrategy != null )
        {
            aggregationStrategy.Aggregate(frame, destinationMemory, BitsPerSample);
        }
        else
        {
            SampleSerializer.Serialize(frame, ByteFrames.AsMemory(startByte), BitsPerSample);
        }
    }

    private void AddFrameToSamples(ReadOnlyMemory<float> frame)
    {
        if ( FloatFrames!.Length <= FramesCount * ChannelCount )
        {
            Array.Resize(ref FloatFrames, FloatFrames.Length * 2);
        }

        var destinationMemory = FloatFrames.AsMemory((int)(FramesCount * ChannelCount));
        if ( aggregationStrategy != null )
        {
            aggregationStrategy.Aggregate(frame, destinationMemory);
        }
        else
        {
            frame.Span.CopyTo(destinationMemory.Span);
        }
    }
}

using System.Numerics;
using System.Text.Json;

using PersonaEngine.Lib.Live2D.Framework.Core;
using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Physics;

/// <summary>
///     物理演算のクラス。
/// </summary>
public class CubismPhysics
{
    /// physics types tags.
    public const string PhysicsTypeTagX = "X";

    public const string PhysicsTypeTagY = "Y";

    public const string PhysicsTypeTagAngle = "Angle";

    /// Constant of air resistance.
    public const float AirResistance = 5.0f;

    /// Constant of maximum weight of input and output ratio.
    public const float MaximumWeight = 100.0f;

    /// Constant of threshold of movement.
    public const float MovementThreshold = 0.001f;

    /// Constant of maximum allowed delta time
    public const float MaxDeltaTime = 5.0f;

    /// <summary>
    ///     最新の振り子計算の結果
    /// </summary>
    private readonly List<float[]> _currentRigOutputs = [];

    /// <summary>
    ///     物理演算のデータ
    /// </summary>
    private readonly CubismPhysicsRig _physicsRig;

    /// <summary>
    ///     一つ前の振り子計算の結果
    /// </summary>
    private readonly List<float[]> _previousRigOutputs = [];

    /// <summary>
    ///     物理演算が処理していない時間
    /// </summary>
    private float _currentRemainTime;

    /// <summary>
    ///     Evaluateで利用するパラメータのキャッシュs
    /// </summary>
    private float[] _parameterCaches = [];

    /// <summary>
    ///     UpdateParticlesが動くときの入力をキャッシュ
    /// </summary>
    private float[] _parameterInputCaches = [];

    /// <summary>
    ///     重力方向
    /// </summary>
    public Vector2 Gravity;

    /// <summary>
    ///     風の方向
    /// </summary>
    public Vector2 Wind;

    /// <summary>
    ///     インスタンスを作成する。
    /// </summary>
    /// <param name="buffer">physics3.jsonが読み込まれいるバッファ</param>
    public CubismPhysics(string buffer)
    {
        // set default options.
        Gravity.Y          = -1.0f;
        Gravity.X          = 0;
        Wind.X             = 0;
        Wind.Y             = 0;
        _currentRemainTime = 0.0f;

        using var stream = File.Open(buffer, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        var obj = JsonSerializer.Deserialize(stream, CubismPhysicsObjContext.Default.CubismPhysicsObj)
                  ?? throw new Exception("Load Physics error");

        _physicsRig = new CubismPhysicsRig {
                                               Gravity     = obj.Meta.EffectiveForces.Gravity,
                                               Wind        = obj.Meta.EffectiveForces.Wind,
                                               SubRigCount = obj.Meta.PhysicsSettingCount,
                                               Fps         = obj.Meta.Fps,
                                               Settings    = new CubismPhysicsSubRig[obj.Meta.PhysicsSettingCount],
                                               Inputs      = new CubismPhysicsInput[obj.Meta.TotalInputCount],
                                               Outputs     = new CubismPhysicsOutput[obj.Meta.TotalOutputCount],
                                               Particles   = new CubismPhysicsParticle[obj.Meta.VertexCount]
                                           };

        _currentRigOutputs.Clear();
        _previousRigOutputs.Clear();

        int inputIndex    = 0,
            outputIndex   = 0,
            particleIndex = 0;

        for ( var i = 0; i < _physicsRig.Settings.Length; ++i )
        {
            var set = obj.PhysicsSettings[i];
            _physicsRig.Settings[i] = new CubismPhysicsSubRig {
                                                                  NormalizationPosition = new CubismPhysicsNormalization { Minimum = set.Normalization.Position.Minimum, Maximum = set.Normalization.Position.Maximum, Default = set.Normalization.Position.Default },
                                                                  NormalizationAngle    = new CubismPhysicsNormalization { Minimum = set.Normalization.Angle.Minimum, Maximum    = set.Normalization.Angle.Maximum, Default    = set.Normalization.Angle.Default },
                                                                  // Input
                                                                  InputCount     = set.Input.Count,
                                                                  BaseInputIndex = inputIndex
                                                              };

            for ( var j = 0; j < _physicsRig.Settings[i].InputCount; ++j )
            {
                var input = set.Input[j];
                _physicsRig.Inputs[inputIndex + j] = new CubismPhysicsInput { SourceParameterIndex = -1, Weight = input.Weight, Reflect = input.Reflect, Source = new CubismPhysicsParameter { TargetType = CubismPhysicsTargetType.CubismPhysicsTargetType_Parameter, Id = input.Source.Id } };

                if ( input.Type == PhysicsTypeTagX )
                {
                    _physicsRig.Inputs[inputIndex + j].Type = CubismPhysicsSource.CubismPhysicsSource_X;
                    _physicsRig.Inputs[inputIndex + j].GetNormalizedParameterValue =
                        GetInputTranslationXFromNormalizedParameterValue;
                }
                else if ( input.Type == PhysicsTypeTagY )
                {
                    _physicsRig.Inputs[inputIndex + j].Type = CubismPhysicsSource.CubismPhysicsSource_Y;
                    _physicsRig.Inputs[inputIndex + j].GetNormalizedParameterValue =
                        GetInputTranslationYFromNormalizedParameterValue;
                }
                else if ( input.Type == PhysicsTypeTagAngle )
                {
                    _physicsRig.Inputs[inputIndex + j].Type = CubismPhysicsSource.CubismPhysicsSource_Angle;
                    _physicsRig.Inputs[inputIndex + j].GetNormalizedParameterValue =
                        GetInputAngleFromNormalizedParameterValue;
                }
            }

            inputIndex += _physicsRig.Settings[i].InputCount;

            // Output
            _physicsRig.Settings[i].OutputCount     = set.Output.Count;
            _physicsRig.Settings[i].BaseOutputIndex = outputIndex;

            _currentRigOutputs.Add(new float[set.Output.Count]);
            _previousRigOutputs.Add(new float[set.Output.Count]);

            for ( var j = 0; j < _physicsRig.Settings[i].OutputCount; ++j )
            {
                var output = set.Output[j];
                _physicsRig.Outputs[outputIndex + j] = new CubismPhysicsOutput {
                                                                                DestinationParameterIndex = -1,
                                                                                VertexIndex               = output.VertexIndex,
                                                                                AngleScale                = output.Scale,
                                                                                Weight                    = output.Weight,
                                                                                Destination               = new CubismPhysicsParameter { TargetType = CubismPhysicsTargetType.CubismPhysicsTargetType_Parameter, Id = output.Destination.Id },
                                                                                Reflect                   = output.Reflect
                                                                               };

                var key = output.Type;
                if ( key == PhysicsTypeTagX )
                {
                    _physicsRig.Outputs[outputIndex + j].Type     = CubismPhysicsSource.CubismPhysicsSource_X;
                    _physicsRig.Outputs[outputIndex + j].GetValue = GetOutputTranslationX;
                    _physicsRig.Outputs[outputIndex + j].GetScale = GetOutputScaleTranslationX;
                }
                else if ( key == PhysicsTypeTagY )
                {
                    _physicsRig.Outputs[outputIndex + j].Type     = CubismPhysicsSource.CubismPhysicsSource_Y;
                    _physicsRig.Outputs[outputIndex + j].GetValue = GetOutputTranslationY;
                    _physicsRig.Outputs[outputIndex + j].GetScale = GetOutputScaleTranslationY;
                }
                else if ( key == PhysicsTypeTagAngle )
                {
                    _physicsRig.Outputs[outputIndex + j].Type     = CubismPhysicsSource.CubismPhysicsSource_Angle;
                    _physicsRig.Outputs[outputIndex + j].GetValue = GetOutputAngle;
                    _physicsRig.Outputs[outputIndex + j].GetScale = GetOutputScaleAngle;
                }
            }

            outputIndex += _physicsRig.Settings[i].OutputCount;

            // Particle
            _physicsRig.Settings[i].ParticleCount     = set.Vertices.Count;
            _physicsRig.Settings[i].BaseParticleIndex = particleIndex;
            for ( var j = 0; j < _physicsRig.Settings[i].ParticleCount; ++j )
            {
                var par = set.Vertices[j];
                _physicsRig.Particles[particleIndex + j] = new CubismPhysicsParticle {
                                                                                Mobility     = par.Mobility,
                                                                                Delay        = par.Delay,
                                                                                Acceleration = par.Acceleration,
                                                                                Radius       = par.Radius,
                                                                                Position     = par.Position
                                                                                };
            }

            particleIndex += _physicsRig.Settings[i].ParticleCount;
        }

        Initialize();

        _physicsRig.Gravity.Y = 0;
    }

    /// <summary>
    ///     パラメータをリセットする。
    /// </summary>
    public void Reset()
    {
        // set default options.
        Gravity.Y = -1.0f;
        Gravity.X = 0.0f;
        Wind.X    = 0.0f;
        Wind.Y    = 0.0f;

        _physicsRig.Gravity.X = 0.0f;
        _physicsRig.Gravity.Y = 0.0f;
        _physicsRig.Wind.X    = 0.0f;
        _physicsRig.Wind.Y    = 0.0f;

        Initialize();
    }

    /// <summary>
    ///     現在のパラメータ値で物理演算が安定化する状態を演算する。
    /// </summary>
    /// <param name="model">物理演算の結果を適用するモデル</param>
    public unsafe void Stabilization(CubismModel model)
    {
        float   totalAngle;
        float   weight;
        float   radAngle;
        float   outputValue;
        Vector2 totalTranslation;
        int i,
            settingIndex,
            particleIndex;

        float* parameterValues;
        float* parameterMaximumValues;
        float* parameterMinimumValues;
        float* parameterDefaultValues;

        parameterValues        = CubismCore.GetParameterValues(model.Model);
        parameterMaximumValues = CubismCore.GetParameterMaximumValues(model.Model);
        parameterMinimumValues = CubismCore.GetParameterMinimumValues(model.Model);
        parameterDefaultValues = CubismCore.GetParameterDefaultValues(model.Model);

        if ( _parameterCaches.Length < model.GetParameterCount() )
        {
            _parameterCaches = new float[model.GetParameterCount()];
        }

        if ( _parameterInputCaches.Length < model.GetParameterCount() )
        {
            _parameterInputCaches = new float[model.GetParameterCount()];
        }

        for ( var j = 0; j < model.GetParameterCount(); ++j )
        {
            _parameterCaches[j]      = parameterValues[j];
            _parameterInputCaches[j] = parameterValues[j];
        }

        for ( settingIndex = 0; settingIndex < _physicsRig.SubRigCount; ++settingIndex )
        {
            totalAngle         = 0.0f;
            totalTranslation.X = 0.0f;
            totalTranslation.Y = 0.0f;

            var currentSetting       = _physicsRig.Settings[settingIndex];
            var currentInputIndex    = currentSetting.BaseInputIndex;
            var currentOutputIndex   = currentSetting.BaseOutputIndex;
            var currentParticleIndex = currentSetting.BaseParticleIndex;

            // Load input parameters
            for ( i = 0; i < currentSetting.InputCount; ++i )
            {
                weight = _physicsRig.Inputs[i + currentInputIndex].Weight / MaximumWeight;

                if ( _physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex == -1 )
                {
                    _physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex = model.GetParameterIndex(_physicsRig.Inputs[i + currentInputIndex].Source.Id);
                }

                _physicsRig.Inputs[i + currentInputIndex].GetNormalizedParameterValue(
                                                                                ref totalTranslation,
                                                                                ref totalAngle,
                                                                                parameterValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                parameterMinimumValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                parameterMaximumValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                parameterDefaultValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                currentSetting.NormalizationPosition,
                                                                                currentSetting.NormalizationAngle,
                                                                                _physicsRig.Inputs[i + currentInputIndex].Reflect,
                                                                                weight
                                                                                );

                _parameterCaches[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex] =
                    parameterValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex];
            }

            radAngle = CubismMath.DegreesToRadian(-totalAngle);

            totalTranslation.X = totalTranslation.X * MathF.Cos(radAngle) - totalTranslation.Y * MathF.Sin(radAngle);
            totalTranslation.Y = totalTranslation.X * MathF.Sin(radAngle) + totalTranslation.Y * MathF.Cos(radAngle);

            // Calculate particles position.
            UpdateParticlesForStabilization(
                                            _physicsRig.Particles,
                                            currentSetting.BaseParticleIndex,
                                            currentSetting.ParticleCount,
                                            totalTranslation,
                                            totalAngle,
                                            Wind,
                                            MovementThreshold * currentSetting.NormalizationPosition.Maximum
                                           );

            // Update output parameters.
            for ( i = 0; i < currentSetting.OutputCount; ++i )
            {
                particleIndex = _physicsRig.Outputs[i + currentOutputIndex].VertexIndex;

                if ( _physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex == -1 )
                {
                    _physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex = model.GetParameterIndex(
                                                                                _physicsRig.Outputs[i + currentOutputIndex].Destination.Id);
                }

                if ( particleIndex < 1 || particleIndex >= currentSetting.ParticleCount )
                {
                    continue;
                }

                Vector2 translation = new() { X = _physicsRig.Particles[particleIndex + currentParticleIndex].Position.X - _physicsRig.Particles[particleIndex - 1 + currentParticleIndex].Position.X, Y = _physicsRig.Particles[particleIndex + currentParticleIndex].Position.Y - _physicsRig.Particles[particleIndex - 1 + currentParticleIndex].Position.Y };

                outputValue = _physicsRig.Outputs[i + currentOutputIndex].GetValue(
                                                                                translation,
                                                                                _physicsRig.Particles,
                                                                                currentParticleIndex,
                                                                                particleIndex,
                                                                                _physicsRig.Outputs[i + currentOutputIndex].Reflect,
                                                                                Gravity
                                                                                );

                _currentRigOutputs[settingIndex][i]  = outputValue;
                _previousRigOutputs[settingIndex][i] = outputValue;

                UpdateOutputParameterValue(
                                           ref parameterValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                           parameterMinimumValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                           parameterMaximumValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                           outputValue,
                                           _physicsRig.Outputs[i + currentOutputIndex]);

                _parameterCaches[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex] = parameterValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex];
            }
        }
    }

    /// <summary>
    ///     物理演算を評価する。
    ///     Pendulum interpolation weights
    ///     振り子の計算結果は保存され、パラメータへの出力は保存された前回の結果で補間されます。
    ///     The result of the pendulum calculation is saved and
    ///     the output to the parameters is interpolated with the saved previous result of the pendulum calculation.
    ///     図で示すと[1]と[2]で補間されます。
    ///     The figure shows the interpolation between [1] and [2].
    ///     補間の重みは最新の振り子計算タイミングと次回のタイミングの間で見た現在時間で決定する。
    ///     The weight of the interpolation are determined by the current time seen between
    ///     the latest pendulum calculation timing and the next timing.
    ///     図で示すと[2]と[4]の間でみた(3)の位置の重みになる。
    ///     Figure shows the weight of position (3) as seen between [2] and [4].
    ///     解釈として振り子計算のタイミングと重み計算のタイミングがズレる。
    ///     As an interpretation, the pendulum calculation and weights are misaligned.
    ///     physics3.jsonにFPS情報が存在しない場合は常に前の振り子状態で設定される。
    ///     If there is no FPS information in physics3.json, it is always set in the previous pendulum state.
    ///     この仕様は補間範囲を逸脱したことが原因の震えたような見た目を回避を目的にしている。
    ///     The purpose of this specification is to avoid the quivering appearance caused by deviations from the interpolation
    ///     range.
    ///     ------------ time -------------->
    ///     |+++++|------|
    ///     <- weight
    ///         ==[1]====#=====[2]---(3)----(4)
    ///         ^ output contents
    ///
    ///         1 :_previousRigOutputs
    ///         2 :_currentRigOutputs
    ///         3 :_currentRemainTime ( now rendering)
    ///         4 :next particles timing
    ///
    ///         @ param model
    ///         @ param deltaTimeSeconds rendering delta time.
    /// </summary>
    /// <param name="model">物理演算の結果を適用するモデル</param>
    /// <param name="deltaTimeSeconds">デルタ時間[秒]</param>
    public unsafe void Evaluate(CubismModel model, float deltaTimeSeconds)
    {
        float   totalAngle;
        float   weight;
        float   radAngle;
        float   outputValue;
        Vector2 totalTranslation;
        int i,
            settingIndex,
            particleIndex;

        if ( 0.0f >= deltaTimeSeconds )
        {
            return;
        }

        float physicsDeltaTime;
        _currentRemainTime += deltaTimeSeconds;
        if ( _currentRemainTime > MaxDeltaTime )
        {
            _currentRemainTime = 0.0f;
        }

        var parameterValues        = CubismCore.GetParameterValues(model.Model);
        var parameterMaximumValues = CubismCore.GetParameterMaximumValues(model.Model);
        var parameterMinimumValues = CubismCore.GetParameterMinimumValues(model.Model);
        var parameterDefaultValues = CubismCore.GetParameterDefaultValues(model.Model);

        if ( _parameterCaches.Length < model.GetParameterCount() )
        {
            _parameterCaches = new float[model.GetParameterCount()];
        }

        if ( _parameterInputCaches.Length < model.GetParameterCount() )
        {
            _parameterInputCaches = new float[model.GetParameterCount()];
            for ( var j = 0; j < model.GetParameterCount(); ++j )
            {
                _parameterInputCaches[j] = parameterValues[j];
            }
        }

        if ( _physicsRig.Fps > 0.0f )
        {
            physicsDeltaTime = 1.0f / _physicsRig.Fps;
        }
        else
        {
            physicsDeltaTime = deltaTimeSeconds;
        }

        CubismPhysicsSubRig currentSetting;
        CubismPhysicsOutput currentOutputs;

        while ( _currentRemainTime >= physicsDeltaTime )
        {
            // copyRigOutputs _currentRigOutputs to _previousRigOutputs
            for ( settingIndex = 0; settingIndex < _physicsRig.SubRigCount; ++settingIndex )
            {
                currentSetting = _physicsRig.Settings[settingIndex];
                if ( currentSetting.BaseOutputIndex >= _physicsRig.Outputs.Length )
                {
                    continue;
                }

                currentOutputs = _physicsRig.Outputs[currentSetting.BaseOutputIndex];
                for ( i = 0; i < currentSetting.OutputCount; ++i )
                {
                    _previousRigOutputs[settingIndex][i] = _currentRigOutputs[settingIndex][i];
                }
            }

            // 入力キャッシュとパラメータで線形補間してUpdateParticlesするタイミングでの入力を計算する。
            // Calculate the input at the timing to UpdateParticles by linear interpolation with the _parameterInputCaches and parameterValues.
            // _parameterCachesはグループ間での値の伝搬の役割があるので_parameterInputCachesとの分離が必要。
            // _parameterCaches needs to be separated from _parameterInputCaches because of its role in propagating values between groups.
            var inputWeight = physicsDeltaTime / _currentRemainTime;
            for ( var j = 0; j < model.GetParameterCount(); ++j )
            {
                _parameterCaches[j]      = _parameterInputCaches[j] * (1.0f - inputWeight) + parameterValues[j] * inputWeight;
                _parameterInputCaches[j] = _parameterCaches[j];
            }

            for ( settingIndex = 0; settingIndex < _physicsRig.SubRigCount; ++settingIndex )
            {
                totalAngle         = 0.0f;
                totalTranslation.X = 0.0f;
                totalTranslation.Y = 0.0f;
                currentSetting     = _physicsRig.Settings[settingIndex];
                var currentInputIndex    = currentSetting.BaseInputIndex;
                var currentOutputIndex   = currentSetting.BaseOutputIndex;
                var currentParticleIndex = currentSetting.BaseParticleIndex;

                // Load input parameters.
                for ( i = 0; i < currentSetting.InputCount; ++i )
                {
                    weight = _physicsRig.Inputs[i + currentInputIndex].Weight / MaximumWeight;

                    if ( _physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex == -1 )
                    {
                        _physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex = model.GetParameterIndex(_physicsRig.Inputs[i + currentInputIndex].Source.Id);
                    }

                    _physicsRig.Inputs[i + currentInputIndex].GetNormalizedParameterValue(
                                                                                ref totalTranslation,
                                                                                ref totalAngle,
                                                                                _parameterCaches[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                parameterMinimumValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                parameterMaximumValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                parameterDefaultValues[_physicsRig.Inputs[i + currentInputIndex].SourceParameterIndex],
                                                                                currentSetting.NormalizationPosition,
                                                                                currentSetting.NormalizationAngle,
                                                                                _physicsRig.Inputs[i + currentInputIndex].Reflect,
                                                                                weight
                                                                                );
                }

                radAngle = CubismMath.DegreesToRadian(-totalAngle);

                totalTranslation.X = totalTranslation.X * MathF.Cos(radAngle) - totalTranslation.Y * MathF.Sin(radAngle);
                totalTranslation.Y = totalTranslation.X * MathF.Sin(radAngle) + totalTranslation.Y * MathF.Cos(radAngle);

                // Calculate particles position.
                UpdateParticles(
                                _physicsRig.Particles,
                                currentParticleIndex,
                                currentSetting.ParticleCount,
                                totalTranslation,
                                totalAngle,
                                Wind,
                                MovementThreshold * currentSetting.NormalizationPosition.Maximum,
                                physicsDeltaTime,
                                AirResistance
                               );

                // Update output parameters.
                for ( i = 0; i < currentSetting.OutputCount; ++i )
                {
                    particleIndex = _physicsRig.Outputs[i + currentOutputIndex].VertexIndex;

                    if ( _physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex == -1 )
                    {
                        _physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex = model.GetParameterIndex(_physicsRig.Outputs[i + currentOutputIndex].Destination.Id);
                    }

                    if ( particleIndex < 1 || particleIndex >= currentSetting.ParticleCount )
                    {
                        continue;
                    }

                    Vector2 translation;
                    translation.X = _physicsRig.Particles[particleIndex + currentParticleIndex].Position.X - _physicsRig.Particles[particleIndex - 1 + currentParticleIndex].Position.X;
                    translation.Y = _physicsRig.Particles[particleIndex + currentParticleIndex].Position.Y - _physicsRig.Particles[particleIndex - 1 + currentParticleIndex].Position.Y;

                    outputValue = _physicsRig.Outputs[i + currentOutputIndex].GetValue(
                                                                                translation,
                                                                                _physicsRig.Particles,
                                                                                currentParticleIndex,
                                                                                particleIndex,
                                                                                _physicsRig.Outputs[i + currentOutputIndex].Reflect,
                                                                                Gravity
                                                                                );

                    _currentRigOutputs[settingIndex][i] = outputValue;

                    UpdateOutputParameterValue(
                                               ref _parameterCaches[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                               parameterMinimumValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                               parameterMaximumValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                               outputValue,
                                               _physicsRig.Outputs[i + currentOutputIndex]);
                }
            }

            _currentRemainTime -= physicsDeltaTime;
        }

        var alpha = _currentRemainTime / physicsDeltaTime;
        Interpolate(model, alpha);
    }

    /// <summary>
    ///     オプションを設定する。
    /// </summary>
    public void SetOptions(Vector2 gravity, Vector2 wind)
    {
        Gravity = gravity;
        Wind    = wind;
    }

    /// <summary>
    ///     オプションを取得する。
    /// </summary>
    /// <returns>オプション</returns>
    public (Vector2 Gravity, Vector2 Wind) GetOptions() { return (Gravity, Wind); }

    /// <summary>
    ///     初期化する。
    /// </summary>
    private void Initialize()
    {
        CubismPhysicsSubRig currentSetting;
        int i,
            settingIndex;

        Vector2 radius;

        for ( settingIndex = 0; settingIndex < _physicsRig.SubRigCount; ++settingIndex )
        {
            currentSetting = _physicsRig.Settings[settingIndex];
            var index = currentSetting.BaseParticleIndex;

            // Initialize the top of particle.
            _physicsRig.Particles[index].InitialPosition =  new Vector2(0.0f, 0.0f);
            _physicsRig.Particles[index].LastPosition    =  _physicsRig.Particles[index].InitialPosition;
            _physicsRig.Particles[index].LastGravity     =  new Vector2(0.0f, -1.0f);
            _physicsRig.Particles[index].LastGravity.Y   *= -1.0f;
            _physicsRig.Particles[index].Velocity        =  new Vector2(0.0f, 0.0f);
            _physicsRig.Particles[index].Force           =  new Vector2(0.0f, 0.0f);

            // Initialize particles.
            for ( i = 1; i < currentSetting.ParticleCount; ++i )
            {
                radius                                           =  new Vector2(0.0f, _physicsRig.Particles[i + index].Radius);
                _physicsRig.Particles[i + index].InitialPosition =  _physicsRig.Particles[i - 1 + index].InitialPosition + radius;
                _physicsRig.Particles[i + index].Position        =  _physicsRig.Particles[i + index].InitialPosition;
                _physicsRig.Particles[i + index].LastPosition    =  _physicsRig.Particles[i + index].InitialPosition;
                _physicsRig.Particles[i + index].LastGravity     =  new Vector2(0.0f, -1.0f);
                _physicsRig.Particles[i + index].LastGravity.Y   *= -1.0f;
                _physicsRig.Particles[i + index].Velocity        =  new Vector2(0.0f, 0.0f);
                _physicsRig.Particles[i + index].Force           =  new Vector2(0.0f, 0.0f);
            }
        }
    }

    /// <summary>
    ///     振り子演算の最新の結果と一つ前の結果から指定した重みで適用する。
    /// </summary>
    /// <param name="model">物理演算の結果を適用するモデル</param>
    /// <param name="weight">最新結果の重み</param>
    private unsafe void Interpolate(CubismModel model, float weight)
    {
        int i,
            settingIndex;

        float* parameterValues;
        float* parameterMaximumValues;
        float* parameterMinimumValues;

        int                 currentOutputIndex;
        CubismPhysicsSubRig currentSetting;

        parameterValues        = CubismCore.GetParameterValues(model.Model);
        parameterMaximumValues = CubismCore.GetParameterMaximumValues(model.Model);
        parameterMinimumValues = CubismCore.GetParameterMinimumValues(model.Model);

        for ( settingIndex = 0; settingIndex < _physicsRig.SubRigCount; ++settingIndex )
        {
            currentSetting     = _physicsRig.Settings[settingIndex];
            currentOutputIndex = currentSetting.BaseOutputIndex;

            // Load input parameters.
            for ( i = 0; i < currentSetting.OutputCount; ++i )
            {
                if ( _physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex == -1 )
                {
                    continue;
                }

                var value = _previousRigOutputs[settingIndex][i] * (1 - weight) + _currentRigOutputs[settingIndex][i] * weight;

                UpdateOutputParameterValue(
                                           ref parameterValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                           parameterMinimumValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                           parameterMaximumValues[_physicsRig.Outputs[i + currentOutputIndex].DestinationParameterIndex],
                                           value,
                                           _physicsRig.Outputs[i + currentOutputIndex]
                                          );
            }
        }
    }

    private float GetRangeValue(float min, float max)
    {
        var maxValue = CubismMath.Max(min, max);
        var minValue = CubismMath.Min(min, max);

        return MathF.Abs(maxValue - minValue);
    }

    /// Gets sign.
    ///
    /// @param  value  Evaluation target value.
    ///
    /// @return  Sign of value.
    private static int Sign(float value)
    {
        var ret = 0;

        if ( value > 0.0f )
        {
            ret = 1;
        }
        else if ( value < 0.0f )
        {
            ret = -1;
        }

        return ret;
    }

    private float GetDefaultValue(float min, float max)
    {
        var minValue = CubismMath.Min(min, max);

        return minValue + GetRangeValue(min, max) / 2.0f;
    }

    private float NormalizeParameterValue(
        float value,
        float parameterMinimum,
        float parameterMaximum,
        float parameterDefault,
        float normalizedMinimum,
        float normalizedMaximum,
        float normalizedDefault,
        bool  isInverted)
    {
        var result = 0.0f;

        var maxValue = CubismMath.Max(parameterMaximum, parameterMinimum);

        if ( maxValue < value )
        {
            value = maxValue;
        }

        var minValue = CubismMath.Min(parameterMaximum, parameterMinimum);

        if ( minValue > value )
        {
            value = minValue;
        }

        var minNormValue    = CubismMath.Min(normalizedMinimum, normalizedMaximum);
        var maxNormValue    = CubismMath.Max(normalizedMinimum, normalizedMaximum);
        var middleNormValue = normalizedDefault;

        var middleValue = GetDefaultValue(minValue, maxValue);
        var paramValue  = value - middleValue;

        switch ( Sign(paramValue) )
        {
            case 1:
            {
                var nLength = maxNormValue - middleNormValue;
                var pLength = maxValue - middleValue;
                if ( pLength != 0.0f )
                {
                    result =  paramValue * (nLength / pLength);
                    result += middleNormValue;
                }

                break;
            }
            case -1:
            {
                var nLength = minNormValue - middleNormValue;
                var pLength = minValue - middleValue;
                if ( pLength != 0.0f )
                {
                    result =  paramValue * (nLength / pLength);
                    result += middleNormValue;
                }

                break;
            }
            case 0:
            {
                result = middleNormValue;

                break;
            }
        }

        return isInverted ? result : result * -1.0f;
    }

    private void GetInputTranslationXFromNormalizedParameterValue(ref Vector2                targetTranslation,     ref float targetAngle, float value,
                                                                  float                      parameterMinimumValue, float     parameterMaximumValue,
                                                                  float                      parameterDefaultValue,
                                                                  CubismPhysicsNormalization normalizationPosition,
                                                                  CubismPhysicsNormalization normalizationAngle, bool isInverted,
                                                                  float                      weight)
    {
        targetTranslation.X += NormalizeParameterValue(
                                                       value,
                                                       parameterMinimumValue,
                                                       parameterMaximumValue,
                                                       parameterDefaultValue,
                                                       normalizationPosition.Minimum,
                                                       normalizationPosition.Maximum,
                                                       normalizationPosition.Default,
                                                       isInverted
                                                      ) * weight;
    }

    private void GetInputTranslationYFromNormalizedParameterValue(ref Vector2                targetTranslation,     ref float targetAngle, float value,
                                                                  float                      parameterMinimumValue, float     parameterMaximumValue,
                                                                  float                      parameterDefaultValue,
                                                                  CubismPhysicsNormalization normalizationPosition,
                                                                  CubismPhysicsNormalization normalizationAngle,
                                                                  bool                       isInverted, float weight)
    {
        targetTranslation.Y += NormalizeParameterValue(
                                                       value,
                                                       parameterMinimumValue,
                                                       parameterMaximumValue,
                                                       parameterDefaultValue,
                                                       normalizationPosition.Minimum,
                                                       normalizationPosition.Maximum,
                                                       normalizationPosition.Default,
                                                       isInverted
                                                      ) * weight;
    }

    private void GetInputAngleFromNormalizedParameterValue(ref Vector2                targetTranslation,     ref float targetAngle, float value,
                                                           float                      parameterMinimumValue, float     parameterMaximumValue,
                                                           float                      parameterDefaultValue,
                                                           CubismPhysicsNormalization normalizationPosition,
                                                           CubismPhysicsNormalization normalizationAngle,
                                                           bool                       isInverted, float weight)
    {
        targetAngle += NormalizeParameterValue(
                                               value,
                                               parameterMinimumValue,
                                               parameterMaximumValue,
                                               parameterDefaultValue,
                                               normalizationAngle.Minimum,
                                               normalizationAngle.Maximum,
                                               normalizationAngle.Default,
                                               isInverted
                                              ) * weight;
    }

    private float GetOutputTranslationX(Vector2 translation,   CubismPhysicsParticle[] particles,  int     start,
                                        int     particleIndex, bool                    isInverted, Vector2 parentGravity)
    {
        var outputValue = translation.X;

        if ( isInverted )
        {
            outputValue *= -1.0f;
        }

        return outputValue;
    }

    private float GetOutputTranslationY(Vector2 translation,   CubismPhysicsParticle[] particles,  int     start,
                                        int     particleIndex, bool                    isInverted, Vector2 parentGravity)
    {
        var outputValue = translation.Y;

        if ( isInverted )
        {
            outputValue *= -1.0f;
        }

        return outputValue;
    }

    private float GetOutputAngle(Vector2 translation,   CubismPhysicsParticle[] particles,  int     start,
                                 int     particleIndex, bool                    isInverted, Vector2 parentGravity)
    {
        float outputValue;

        if ( particleIndex >= 2 )
        {
            parentGravity = particles[particleIndex - 1 + start].Position - particles[particleIndex - 2 + start].Position;
        }
        else
        {
            parentGravity *= -1.0f;
        }

        outputValue = CubismMath.DirectionToRadian(parentGravity, translation);

        if ( isInverted )
        {
            outputValue *= -1.0f;
        }

        return outputValue;
    }

    private float GetOutputScaleTranslationX(Vector2 translationScale, float angleScale) { return translationScale.X; }

    private float GetOutputScaleTranslationY(Vector2 translationScale, float angleScale) { return translationScale.Y; }

    private float GetOutputScaleAngle(Vector2 translationScale, float angleScale) { return angleScale; }

    /// Updates particles.
    ///
    /// @param  strand            Target array of particle.
    /// @param  strandCount       Count of particle.
    /// @param  totalTranslation  Total translation value.
    /// @param  totalAngle        Total angle.
    /// @param  windDirection              Direction of wind.
    /// @param  thresholdValue    Threshold of movement.
    /// @param  deltaTimeSeconds  Delta time.
    /// @param  airResistance     Air resistance.
    private static void UpdateParticles(CubismPhysicsParticle[] strand,        int   start,          int   strandCount,      Vector2 totalTranslation, float totalAngle,
                                        Vector2                 windDirection, float thresholdValue, float deltaTimeSeconds, float   airResistance)
    {
        int     i;
        float   totalRadian;
        float   delay;
        float   radian;
        Vector2 currentGravity;
        Vector2 direction;
        Vector2 velocity;
        Vector2 force;
        Vector2 newDirection;

        strand[start].Position = totalTranslation;

        totalRadian    = CubismMath.DegreesToRadian(totalAngle);
        currentGravity = CubismMath.RadianToDirection(totalRadian);
        currentGravity = Vector2.Normalize(currentGravity);

        for ( i = 1; i < strandCount; ++i )
        {
            strand[i + start].Force = currentGravity * strand[i + start].Acceleration + windDirection;

            strand[i + start].LastPosition = strand[i + start].Position;

            delay = strand[i + start].Delay * deltaTimeSeconds * 30.0f;

            direction.X = strand[i + start].Position.X - strand[i - 1 + start].Position.X;
            direction.Y = strand[i + start].Position.Y - strand[i - 1 + start].Position.Y;

            radian = CubismMath.DirectionToRadian(strand[i + start].LastGravity, currentGravity) / airResistance;

            direction.X = MathF.Cos(radian) * direction.X - direction.Y * MathF.Sin(radian);
            direction.Y = MathF.Sin(radian) * direction.X + direction.Y * MathF.Cos(radian);

            strand[i + start].Position = strand[i - 1 + start].Position + direction;

            velocity.X = strand[i + start].Velocity.X * delay;
            velocity.Y = strand[i + start].Velocity.Y * delay;
            force      = strand[i + start].Force * delay * delay;

            strand[i + start].Position = strand[i + start].Position + velocity + force;

            newDirection = strand[i + start].Position - strand[i - 1 + start].Position;

            newDirection = Vector2.Normalize(newDirection);

            strand[i + start].Position = strand[i - 1 + start].Position + newDirection * strand[i + start].Radius;

            if ( MathF.Abs(strand[i + start].Position.X) < thresholdValue )
            {
                strand[i + start].Position.X = 0.0f;
            }

            if ( delay != 0.0f )
            {
                strand[i + start].Velocity.X =  strand[i + start].Position.X - strand[i + start].LastPosition.X;
                strand[i + start].Velocity.Y =  strand[i + start].Position.Y - strand[i + start].LastPosition.Y;
                strand[i + start].Velocity   /= delay;
                strand[i + start].Velocity   *= strand[i + start].Mobility;
            }

            strand[i + start].Force       = new Vector2(0.0f, 0.0f);
            strand[i + start].LastGravity = currentGravity;
        }
    }

    /**
     * Updates particles for stabilization.
     *
     * @param strand                Target array of particle.
     * @param strandCount           Count of particle.
     * @param totalTranslation      Total translation value.
     * @param totalAngle            Total angle.
     * @param windDirection         Direction of Wind.
     * @param thresholdValue        Threshold of movement.
     */
    private static void UpdateParticlesForStabilization(CubismPhysicsParticle[] strand,        int   start, int strandCount, Vector2 totalTranslation, float totalAngle,
                                                        Vector2                 windDirection, float thresholdValue)
    {
        int     i;
        float   totalRadian;
        Vector2 currentGravity;
        Vector2 force;

        strand[start].Position = totalTranslation;

        totalRadian    = CubismMath.DegreesToRadian(totalAngle);
        currentGravity = CubismMath.RadianToDirection(totalRadian);
        currentGravity = Vector2.Normalize(currentGravity);

        for ( i = 1; i < strandCount; ++i )
        {
            strand[i + start].Force = currentGravity * strand[i + start].Acceleration + windDirection;

            strand[i + start].LastPosition = strand[i + start].Position;

            strand[i + start].Velocity = new Vector2(0.0f, 0.0f);

            force = strand[i + start].Force;
            force = Vector2.Normalize(force);

            force                      *= strand[i + start].Radius;
            strand[i + start].Position =  strand[i - 1].Position + force;

            if ( MathF.Abs(strand[i + start].Position.X) < thresholdValue )
            {
                strand[i + start].Position.X = 0.0f;
            }

            strand[i + start].Force       = new Vector2(0.0f, 0.0f);
            strand[i + start].LastGravity = currentGravity;
        }
    }

    /// Updates output parameter value.
    ///
    /// @param  parameterValue         Target parameter value.
    /// @param  parameterValueMinimum  Minimum of parameter value.
    /// @param  parameterValueMaximum  Maximum of parameter value.
    /// @param  translation            Translation value.
    private static void UpdateOutputParameterValue(ref float parameterValue, float               parameterValueMinimum, float parameterValueMaximum,
                                                   float     translation,    CubismPhysicsOutput output)
    {
        float outputScale;
        float value;
        float weight;

        outputScale = output.GetScale(output.TranslationScale, output.AngleScale);

        value = translation * outputScale;

        if ( value < parameterValueMinimum )
        {
            if ( value < output.ValueBelowMinimum )
            {
                output.ValueBelowMinimum = value;
            }

            value = parameterValueMinimum;
        }
        else if ( value > parameterValueMaximum )
        {
            if ( value > output.ValueExceededMaximum )
            {
                output.ValueExceededMaximum = value;
            }

            value = parameterValueMaximum;
        }

        weight = output.Weight / MaximumWeight;

        if ( weight >= 1.0f )
        {
            parameterValue = value;
        }
        else
        {
            value          = parameterValue * (1.0f - weight) + value * weight;
            parameterValue = value;
        }
    }
}

using System.Collections.Frozen;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;

using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.TTS.Synthesis;

public partial class Lexicon : ILexicon, IDisposable
{
    private static readonly Regex _suffixRegex = SuffixRegex();

    private static readonly Regex _doubleConsonantIngRegex = DoubleConsonantIngRegex();

    private static readonly Regex _vsRegex = VersusRegex();

    private readonly (double Min, double Max) _capStresses = (0.5, 2.0);

    private readonly Lock _dictionaryLock = new();

    private readonly LruCache<string, (string? Phonemes, int? Rating)> _stemCache;

    private readonly IOptionsMonitor<TtsConfiguration> _ttsConfig;

    private readonly IOptionsMonitor<KokoroVoiceOptions> _voiceOptions;

    private readonly IDisposable? _voiceOptionsChangeToken;

    private readonly LruCache<string, (string? Phonemes, int? Rating)> _wordCache;

    private volatile bool _british;

    private volatile IReadOnlyDictionary<string, PhonemeEntry>? _golds;

    private volatile IReadOnlyDictionary<string, PhonemeEntry>? _silvers;

    private volatile HashSet<char> _vocab;

    public Lexicon(
        IOptionsMonitor<TtsConfiguration>   ttsConfig,
        IOptionsMonitor<KokoroVoiceOptions> voiceOptions)
    {
        _ttsConfig    = ttsConfig;
        _voiceOptions = voiceOptions;

        // Initialize caches - size based on typical working set
        _wordCache = new LruCache<string, (string?, int?)>(2048, StringComparer.Ordinal);
        _stemCache = new LruCache<string, (string?, int?)>(512, StringComparer.Ordinal);

        // Initial load of dictionaries
        LoadDictionaries();

        // Register for configuration changes
        _voiceOptionsChangeToken = _voiceOptions.OnChange(options =>
                                                          {
                                                              var currentBritish = options.UseBritishEnglish;
                                                              if ( _british != currentBritish )
                                                              {
                                                                  LoadDictionaries();
                                                              }
                                                          });
    }

    public void Dispose() { _voiceOptionsChangeToken?.Dispose(); }

    public (string? Phonemes, int? Rating) ProcessToken(Token token, TokenContext ctx)
    {
        // Ensure dictionaries are loaded
        if ( _golds == null || _silvers == null )
        {
            LoadDictionaries();
        }

        // Normalize text: replace special quotes with straight quotes and normalize Unicode
        var word = (token.Text ?? token.Alias ?? "").Replace('\u2018', '\'').Replace('\u2019', '\'');
        word = word.Normalize(NormalizationForm.FormKC);

        // Calculate stress based on capitalization
        var stress = token.Stress;
        if ( stress == null && word != word.ToLower() )
        {
            stress = word == word.ToUpper() ? _capStresses.Max : _capStresses.Min;
        }

        // Call GetWord directly - it already handles all the necessary phoneme lookups,
        // stemming, and special cases
        return GetWord(
                       word,
                       token.Tag,
                       stress,
                       ctx,
                       token.IsHead,
                       token.Currency,
                       token.NumFlags);
    }

    private void LoadDictionaries()
    {
        lock (_dictionaryLock)
        {
            var voiceOptions = _voiceOptions.CurrentValue;
            var ttsConfig    = _ttsConfig.CurrentValue;
            var british      = voiceOptions.UseBritishEnglish;

            // Double-check after acquiring lock to avoid unnecessary reloads
            if ( _golds != null && _british == british )
            {
                return;
            }

            // Update settings
            _british = british;
            _vocab   = british ? PhonemizerConstants.GbVocab : PhonemizerConstants.UsVocab;

            // Construct paths
            var lexiconPrefix = british ? "gb" : "us";
            var primaryPath   = Path.Combine(ttsConfig.ModelDirectory, $"{lexiconPrefix}_gold.json");
            var secondaryPath = Path.Combine(ttsConfig.ModelDirectory, $"{lexiconPrefix}_silver.json");

            // Use JsonSerializer with custom options
            var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, Converters = { new PhonemeEntryConverter() } };

            // Load dictionaries
            _golds   = LoadAndGrowDictionary(primaryPath, options);
            _silvers = LoadAndGrowDictionary(secondaryPath, options);

            // Clear caches when dictionaries change
            _wordCache.Clear();
            _stemCache.Clear();
        }
    }

    private IReadOnlyDictionary<string, PhonemeEntry> LoadAndGrowDictionary(string path, JsonSerializerOptions options)
    {
        using var fileStream = File.OpenRead(path);

        // Deserialize with custom converter
        var rawDict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(fileStream);
        if ( rawDict == null )
        {
            throw new InvalidOperationException($"Failed to deserialize lexicon from {path}");
        }

        // Convert to strongly typed entries
        var dict = new Dictionary<string, PhonemeEntry>(StringComparer.OrdinalIgnoreCase);
        foreach ( var (key, element) in rawDict )
        {
            if ( key.Length >= 2 )
            {
                dict[key] = PhonemeEntry.FromJsonElement(element);
            }
        }

        // Grow the dictionary with additional forms
        var extended = new Dictionary<string, PhonemeEntry>(StringComparer.OrdinalIgnoreCase);
        foreach ( var (key, value) in dict )
        {
            if ( key.Length < 2 )
            {
                continue;
            }

            if ( key == key.ToLower() )
            {
                var capitalized = LexiconUtils.Capitalize(key);
                if ( key != capitalized && !dict.ContainsKey(capitalized) )
                {
                    extended[capitalized] = value;
                }
            }
            else if ( key == LexiconUtils.Capitalize(key.ToLower()) )
            {
                var lower = key.ToLower();
                if ( !dict.ContainsKey(lower) )
                {
                    extended[lower] = value;
                }
            }
        }

        // Combine original and extended entries
        foreach ( var (key, value) in extended )
        {
            dict[key] = value;
        }

        // Return as frozen dictionary for performance
        return dict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private (string? Phonemes, int? Rating) GetWord(
        string       word,
        string       tag,
        double?      stress,
        TokenContext ctx,
        bool         isHead,
        string?      currency,
        string       numFlags)
    {
        // Cache lookup - create a composite key for context-dependent lookups
        var cacheKey = $"{word}|{tag}|{stress}|{ctx.FutureVowel}|{ctx.FutureTo}|{currency}|{numFlags}";
        if ( _wordCache.TryGetValue(cacheKey, out var cached) )
        {
            return cached;
        }

        // 1. Check special cases first
        var specialCase = GetSpecialCase(word, tag, stress, ctx);
        if ( specialCase.Phonemes != null )
        {
            var finalResult = (AppendCurrency(specialCase.Phonemes, currency), specialCase.Rating);
            _wordCache[cacheKey] = finalResult;

            return finalResult;
        }

        // 2. Check if word is in dictionaries
        if ( IsKnown(word, tag) )
        {
            var result = Lookup(word, tag, stress, ctx);
            if ( result.Phonemes != null )
            {
                var finalResult = (ApplyStress(AppendCurrency(result.Phonemes, currency), stress), result.Rating);
                _wordCache[cacheKey] = finalResult;

                return finalResult;
            }
        }

        // 3. Check for apostrophe s at the end (possessives)
        if ( EndsWith(word, "s'") && IsKnown(word[..^2] + "'s", tag) )
        {
            var result = Lookup(word[..^2] + "'s", tag, stress, ctx);
            if ( result.Phonemes != null )
            {
                var finalResult = (AppendCurrency(result.Phonemes, currency), result.Rating);
                _wordCache[cacheKey] = finalResult;

                return finalResult;
            }
        }

        // 4. Check for words ending with apostrophe
        if ( EndsWith(word, "'") && IsKnown(word[..^1], tag) )
        {
            var result = Lookup(word[..^1], tag, stress, ctx);
            if ( result.Phonemes != null )
            {
                var finalResult = (AppendCurrency(result.Phonemes, currency), result.Rating);
                _wordCache[cacheKey] = finalResult;

                return finalResult;
            }
        }

        // 5. Try stemming for -s suffix
        var stemS = StemS(word, tag, stress, ctx);
        if ( stemS.Item1 != null )
        {
            var finalResult = (AppendCurrency(stemS.Item1, currency), stemS.Item2);
            _wordCache[cacheKey] = finalResult;

            return finalResult;
        }

        // 6. Try stemming for -ed suffix
        var stemEd = StemEd(word, tag, stress, ctx);
        if ( stemEd.Item1 != null )
        {
            var finalResult = (AppendCurrency(stemEd.Item1, currency), stemEd.Item2);
            _wordCache[cacheKey] = finalResult;

            return finalResult;
        }

        // 7. Try stemming for -ing suffix
        var stemIng = StemIng(word, tag, stress ?? 0.5, ctx);
        if ( stemIng.Item1 != null )
        {
            var finalResult = (AppendCurrency(stemIng.Item1, currency), stemIng.Item2);
            _wordCache[cacheKey] = finalResult;

            return finalResult;
        }

        // 8. Handle numbers
        if ( IsNumber(word, isHead) )
        {
            var result = GetNumber(word, currency, isHead, numFlags);
            _wordCache[cacheKey] = result;

            return result;
        }

        // 9. Handle acronyms and capitalized words
        var isAllUpper = word.Length > 1 && word == word.ToUpper() && word.ToUpper() != word.ToLower();
        if ( isAllUpper )
        {
            var (nnpPhonemes, nnpRating) = GetNNP(word);
            if ( nnpPhonemes != null )
            {
                var finalResult = (AppendCurrency(nnpPhonemes, currency), nnpRating);
                _wordCache[cacheKey] = finalResult;

                return finalResult;
            }
        }

        // 10. Try lowercase version if word has some uppercase letters
        if ( word != word.ToLower() && (word == word.ToUpper() || word[1..] == word[1..].ToLower()) )
        {
            var lowercaseResult = Lookup(word.ToLower(), tag, stress, ctx);
            if ( lowercaseResult.Phonemes != null )
            {
                var finalResult = (AppendCurrency(lowercaseResult.Phonemes, currency), lowercaseResult.Rating);
                _wordCache[cacheKey] = finalResult;

                return finalResult;
            }
        }

        // No match found
        _wordCache[cacheKey] = (null, null);

        return (null, null);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private (string? Phonemes, int? Rating) GetSpecialCase(string word, string tag, double? stress, TokenContext ctx)
    {
        // Handle special cases with optimized lookups
        var wordSpan = word.AsSpan();

        // Special symbols
        if ( tag == "ADD" && PhonemizerConstants.AddSymbols.TryGetValue(word, out var addSymbol) )
        {
            return Lookup(addSymbol, null, -0.5, ctx);
        }

        if ( PhonemizerConstants.Symbols.TryGetValue(word, out var symbol) )
        {
            return Lookup(symbol, null, null, ctx);
        }

        if ( wordSpan.Contains('.') && IsAllLettersOrDots(wordSpan) && MaxSubstringLength(wordSpan, '.') < 3 )
        {
            return GetNNP(word);
        }

        switch ( word )
        {
            case "a":
            case "A" when tag == "DT":
                return ("ɐ", 4);
            case "I" when tag == "PRP":
                return ($"{PhonemizerConstants.SecondaryStress}I", 4);
            case "am" or "Am" or "AM" when tag.StartsWith("NN"):
                return GetNNP(word);
            case "am" or "Am" or "AM" when ctx.FutureVowel != null && word == "am" && stress is not > 0:
                return ("ɐm", 4);
            case "am" or "Am" or "AM" when _golds.TryGetValue("am", out var amEntry) && amEntry is SimplePhonemeEntry simpleAm:
                return (simpleAm.Phoneme, 4);
            case "am" or "Am" or "AM":
                return ("ɐm", 4);
            case ("an" or "An" or "AN") and "AN" when tag.StartsWith("NN"):
                return GetNNP(word);
            case "an" or "An" or "AN":
                return ("ɐn", 4);
            case "by" or "By" or "BY" when LexiconUtils.GetParentTag(tag) == "ADV":
                return ("bˈI", 4);
            case "to":
            case "To":
            case "TO" when tag is "TO" or "IN":
                switch ( ctx.FutureVowel )
                {
                    case null:
                    {
                        if ( _golds.TryGetValue("to", out var toEntry) && toEntry is SimplePhonemeEntry simpleTo )
                        {
                            return (simpleTo.Phoneme, 4);
                        }

                        break;
                    }
                    case false:
                        return ("tə", 4);
                    default:
                        return ("tʊ", 4);
                }

                break;
            case "the":
            case "The":
            case "THE" when tag == "DT":
                return (ctx.FutureVowel == true ? "ði" : "ðə", 4);
        }

        if ( tag == "IN" && _vsRegex.IsMatch(word) )
        {
            return Lookup("versus", null, null, ctx);
        }

        if ( word is "used" or "Used" or "USED" )
        {
            if ( tag is "VBD" or "JJ" && ctx.FutureTo )
            {
                if ( _golds.TryGetValue("used", out var usedEntry) && usedEntry is ContextualPhonemeEntry contextualUsed )
                {
                    return (contextualUsed.GetForm("VBD", null), 4);
                }
            }

            if ( _golds.TryGetValue("used", out var usedDefaultEntry) && usedDefaultEntry is ContextualPhonemeEntry contextualDefault )
            {
                return (contextualDefault.GetForm("DEFAULT", null), 4);
            }
        }

        return (null, null);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool IsAllLettersOrDots(ReadOnlySpan<char> word)
    {
        foreach ( var c in word )
        {
            if ( c != '.' && !char.IsLetter(c) )
            {
                return false;
            }
        }

        return true;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private int MaxSubstringLength(ReadOnlySpan<char> text, char separator)
    {
        var maxLength = 0;
        var start     = 0;

        for ( var i = 0; i < text.Length; i++ )
        {
            if ( text[i] == separator )
            {
                maxLength = Math.Max(maxLength, i - start);
                start     = i + 1;
            }
        }

        // Check the last segment
        maxLength = Math.Max(maxLength, text.Length - start);

        return maxLength;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private (string? Phonemes, int? Rating) Lookup(string word, string? tag, double? stress, TokenContext ctx)
    {
        var isNNP = false;
        if ( word == word.ToUpper() && !_golds.ContainsKey(word) )
        {
            word  = word.ToLower();
            isNNP = tag == "NNP";
        }

        PhonemeEntry? entry  = null;
        var           rating = 4;

        if ( _golds.TryGetValue(word, out entry) )
        {
            // Found in gold dictionary
        }
        else if ( !isNNP && _silvers.TryGetValue(word, out entry) )
        {
            rating = 3;
        }

        if ( entry == null )
        {
            if ( isNNP )
            {
                return GetNNP(word);
            }

            return (null, null);
        }

        string? phonemes = null;

        switch ( entry )
        {
            case SimplePhonemeEntry simple:
                phonemes = simple.Phoneme;

                break;

            case ContextualPhonemeEntry contextual:
                phonemes = contextual.GetForm(tag, ctx);

                break;
        }

        if ( phonemes == null || (isNNP && !phonemes.Contains(PhonemizerConstants.PrimaryStress)) )
        {
            return GetNNP(word);
        }

        return (ApplyStress(phonemes, stress), rating);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool IsKnown(string word, string tag)
    {
        if ( _golds.ContainsKey(word) || PhonemizerConstants.Symbols.ContainsKey(word) || _silvers.ContainsKey(word) )
        {
            return true;
        }

        if ( !word.All(c => char.IsLetter(c)) || !word.All(IsValidCharacter) )
        {
            return false;
        }

        if ( word.Length == 1 )
        {
            return true;
        }

        if ( word == word.ToUpper() && _golds.ContainsKey(word.ToLower()) )
        {
            return true;
        }

        return word.Length > 1 && word[1..] == word[1..].ToUpper();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool IsValidCharacter(char c)
    {
        // Check if character is valid for lexicon
        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '\'' || c == '-' || c == '_';
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private string ApplyStress(string phonemes, double? stress)
    {
        if ( phonemes == null )
        {
            return "";
        }

        if ( stress == null )
        {
            return phonemes;
        }

        // Early return conditions
        if ( (stress == 0 || stress == 1) && !ContainsStressMarkers(phonemes) )
        {
            return phonemes;
        }

        // Remove all stress for very negative stress
        if ( stress < -1 )
        {
            return phonemes
                   .Replace(PhonemizerConstants.PrimaryStress.ToString(), string.Empty)
                   .Replace(PhonemizerConstants.SecondaryStress.ToString(), string.Empty);
        }

        // Lower stress level for -1 or 0
        if ( stress == -1 || (stress is 0 or -0.5 && phonemes.Contains(PhonemizerConstants.PrimaryStress)) )
        {
            return phonemes
                   .Replace(PhonemizerConstants.SecondaryStress.ToString(), string.Empty)
                   .Replace(PhonemizerConstants.PrimaryStress.ToString(), PhonemizerConstants.SecondaryStress.ToString());
        }

        // Add secondary stress for unstressed phonemes
        if ( stress is 0 or 0.5 or 1 &&
             !phonemes.Contains(PhonemizerConstants.PrimaryStress) &&
             !phonemes.Contains(PhonemizerConstants.SecondaryStress) )
        {
            if ( !ContainsVowel(phonemes) )
            {
                return phonemes;
            }

            return RestressPhonemes(PhonemizerConstants.SecondaryStress + phonemes);
        }

        // Upgrade secondary stress to primary
        if ( stress >= 1 &&
             !phonemes.Contains(PhonemizerConstants.PrimaryStress) &&
             phonemes.Contains(PhonemizerConstants.SecondaryStress) )
        {
            return phonemes.Replace(
                                    PhonemizerConstants.SecondaryStress.ToString(),
                                    PhonemizerConstants.PrimaryStress.ToString());
        }

        // Add primary stress for high stress values
        if ( stress > 1 &&
             !phonemes.Contains(PhonemizerConstants.PrimaryStress) &&
             !phonemes.Contains(PhonemizerConstants.SecondaryStress) )
        {
            if ( !ContainsVowel(phonemes) )
            {
                return phonemes;
            }

            return RestressPhonemes(PhonemizerConstants.PrimaryStress + phonemes);
        }

        return phonemes;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool ContainsStressMarkers(string phonemes)
    {
        return phonemes.Contains(PhonemizerConstants.PrimaryStress) ||
               phonemes.Contains(PhonemizerConstants.SecondaryStress);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool ContainsVowel(string phonemes) { return phonemes.Any(c => PhonemizerConstants.Vowels.Contains(c)); }

    private string RestressPhonemes(string phonemes)
    {
        // Optimization: Avoid allocations if there are no stress markers
        if ( !ContainsStressMarkers(phonemes) )
        {
            return phonemes;
        }

        var chars         = phonemes.ToCharArray();
        var charPositions = new List<(int Position, char Char)>(chars.Length);

        for ( var i = 0; i < chars.Length; i++ )
        {
            charPositions.Add((i, chars[i]));
        }

        var stressPositions = new Dictionary<int, int>();
        for ( var i = 0; i < charPositions.Count; i++ )
        {
            if ( PhonemizerConstants.Stresses.Contains(charPositions[i].Char) )
            {
                // Find the next vowel
                var vowelPos = -1;
                for ( var j = i + 1; j < charPositions.Count; j++ )
                {
                    if ( PhonemizerConstants.Vowels.Contains(charPositions[j].Char) )
                    {
                        vowelPos = j;

                        break;
                    }
                }

                if ( vowelPos != -1 )
                {
                    stressPositions[charPositions[i].Position] = charPositions[vowelPos].Position;
                    charPositions[i]                           = ((int)(vowelPos - 0.5), charPositions[i].Char);
                }
            }
        }

        charPositions.Sort((a, b) => a.Position.CompareTo(b.Position));

        return new string(charPositions.Select(cp => cp.Char).ToArray());
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool EndsWith(string word, string suffix) { return word.Length > suffix.Length && word.EndsWith(suffix, StringComparison.Ordinal); }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private (string?, int?) GetNNP(string word)
    {
        // Early exit for extremely short or long words
        if ( word.Length < 1 || word.Length > 20 )
        {
            return (null, null);
        }

        // Cache lookups for acronyms
        var cacheKey = $"NNP_{word}";
        if ( _wordCache.TryGetValue(cacheKey, out var cached) )
        {
            return cached;
        }

        // Collect phonemes for each letter
        var phoneticLetters = new List<string>();

        foreach ( var c in word )
        {
            if ( !char.IsLetter(c) )
            {
                continue;
            }

            if ( _golds.TryGetValue(c.ToString().ToUpper(), out var letterEntry) )
            {
                if ( letterEntry is SimplePhonemeEntry simpleEntry )
                {
                    phoneticLetters.Add(simpleEntry.Phoneme);
                }
                else
                {
                    _wordCache[cacheKey] = (null, null);

                    return (null, null);
                }
            }
            else
            {
                _wordCache[cacheKey] = (null, null);

                return (null, null);
            }
        }

        if ( phoneticLetters.Count == 0 )
        {
            _wordCache[cacheKey] = (null, null);

            return (null, null);
        }

        // Join and apply stress
        var phonemes = string.Join("", phoneticLetters);
        var result   = ApplyStress(phonemes, 0);

        // Split by secondary stress and join with primary stress
        // This matches the Python implementation more closely
        var parts = result.Split(PhonemizerConstants.SecondaryStress);
        if ( parts.Length > 1 )
        {
            // Taking only the last split, as in Python's rsplit(SECONDARY_STRESS, 1)
            var lastIdx   = result.LastIndexOf(PhonemizerConstants.SecondaryStress);
            var beginning = result.Substring(0, lastIdx);
            var end       = result.Substring(lastIdx + 1);
            result = beginning + PhonemizerConstants.PrimaryStress + end;
        }

        _wordCache[cacheKey] = (result, 3);

        return (result, 3);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private string? S(string? stem)
    {
        // https://en.wiktionary.org/wiki/-s
        if ( string.IsNullOrEmpty(stem) )
        {
            return null;
        }

        // Cache result for frequently used stems
        var cacheKey = $"S_{stem}";
        if ( _stemCache.TryGetValue(cacheKey, out var cached) && cached.Phonemes != null )
        {
            return cached.Phonemes;
        }

        string? result;
        if ( "ptkfθ".Contains(stem[^1]) )
        {
            result = stem + "s";
        }
        else if ( "szʃʒʧʤ".Contains(stem[^1]) )
        {
            result = stem + (_british ? "ɪ" : "ᵻ") + "z";
        }
        else
        {
            result = stem + "z";
        }

        _stemCache[cacheKey] = (result, null);

        return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private (string?, int?) StemS(string word, string tag, double? stress, TokenContext ctx)
    {
        // Cache lookup
        var cacheKey = $"StemS_{word}_{tag}";
        if ( _stemCache.TryGetValue(cacheKey, out var cached) )
        {
            return cached;
        }

        string stem;

        if ( word.Length > 2 && EndsWith(word, "s") && !EndsWith(word, "ss") && IsKnown(word[..^1], tag) )
        {
            stem = word[..^1];
        }
        else if ( (EndsWith(word, "'s") ||
                   (word.Length > 4 && EndsWith(word, "es") && !EndsWith(word, "ies"))) &&
                  IsKnown(word[..^2], tag) )
        {
            stem = word[..^2];
        }
        else if ( word.Length > 4 && EndsWith(word, "ies") && IsKnown(word[..^3] + "y", tag) )
        {
            stem = word[..^3] + "y";
        }
        else
        {
            _stemCache[cacheKey] = (null, null);

            return (null, null);
        }

        var (stemPhonemes, rating) = Lookup(stem, tag, stress, ctx);
        if ( stemPhonemes != null )
        {
            var result = (S(stemPhonemes), rating);
            _stemCache[cacheKey] = result;

            return result;
        }

        _stemCache[cacheKey] = (null, null);

        return (null, null);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private string? Ed(string? stem)
    {
        // https://en.wiktionary.org/wiki/-ed
        if ( string.IsNullOrEmpty(stem) )
        {
            return null;
        }

        // Cache result for frequently used stems
        var cacheKey = $"Ed_{stem}";
        if ( _stemCache.TryGetValue(cacheKey, out var cached) && cached.Phonemes != null )
        {
            return cached.Phonemes;
        }

        string? result;
        if ( "pkfθʃsʧ".Contains(stem[^1]) )
        {
            result = stem + "t";
        }
        else if ( stem[^1] == 'd' )
        {
            result = stem + (_british ? "ɪ" : "ᵻ") + "d";
        }
        else if ( stem[^1] != 't' )
        {
            result = stem + "d";
        }
        else if ( _british || stem.Length < 2 )
        {
            result = stem + "ɪd";
        }
        else if ( PhonemizerConstants.UsTaus.Contains(stem[^2]) )
        {
            result = stem[..^1] + "ɾᵻd";
        }
        else
        {
            result = stem + "ᵻd";
        }

        _stemCache[cacheKey] = (result, null);

        return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private (string?, int?) StemEd(string word, string tag, double? stress, TokenContext ctx)
    {
        // Cache lookup
        var cacheKey = $"StemEd_{word}_{tag}";
        if ( _stemCache.TryGetValue(cacheKey, out var cached) )
        {
            return cached;
        }

        string stem;

        if ( EndsWith(word, "d") && !EndsWith(word, "dd") && IsKnown(word[..^1], tag) )
        {
            stem = word[..^1];
        }
        else if ( EndsWith(word, "ed") && !EndsWith(word, "eed") && IsKnown(word[..^2], tag) )
        {
            stem = word[..^2];
        }
        else
        {
            _stemCache[cacheKey] = (null, null);

            return (null, null);
        }

        var (stemPhonemes, rating) = Lookup(stem, tag, stress, ctx);
        if ( stemPhonemes != null )
        {
            var result = (Ed(stemPhonemes), rating);
            _stemCache[cacheKey] = result;

            return result;
        }

        _stemCache[cacheKey] = (null, null);

        return (null, null);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private string? Ing(string? stem)
    {
        if ( string.IsNullOrEmpty(stem) )
        {
            return null;
        }

        // Cache result for frequently used stems
        var cacheKey = $"Ing_{stem}";
        if ( _stemCache.TryGetValue(cacheKey, out var cached) && cached.Phonemes != null )
        {
            return cached.Phonemes;
        }

        string? result;
        if ( _british )
        {
            if ( stem[^1] == 'ə' || stem[^1] == 'ː' )
            {
                _stemCache[cacheKey] = (null, null);

                return null;
            }
        }
        else if ( stem.Length > 1 && stem[^1] == 't' && PhonemizerConstants.UsTaus.Contains(stem[^2]) )
        {
            result               = stem[..^1] + "ɾɪŋ";
            _stemCache[cacheKey] = (result, null);

            return result;
        }

        result               = stem + "ɪŋ";
        _stemCache[cacheKey] = (result, null);

        return result;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private (string?, int?) StemIng(string word, string tag, double stress, TokenContext ctx)
    {
        // Cache lookup
        var cacheKey = $"StemIng_{word}_{tag}";
        if ( _stemCache.TryGetValue(cacheKey, out var cached) )
        {
            return cached;
        }

        string stem;

        if ( EndsWith(word, "ing") && IsKnown(word[..^3], tag) )
        {
            stem = word[..^3];
        }
        else if ( EndsWith(word, "ing") && IsKnown(word[..^3] + "e", tag) )
        {
            stem = word[..^3] + "e";
        }
        else if ( _doubleConsonantIngRegex.IsMatch(word) && IsKnown(word[..^4], tag) )
        {
            stem = word[..^4];
        }
        else
        {
            _stemCache[cacheKey] = (null, null);

            return (null, null);
        }

        var (stemPhonemes, rating) = Lookup(stem, tag, stress, ctx);
        if ( stemPhonemes != null )
        {
            var result = (Ing(stemPhonemes), rating);
            _stemCache[cacheKey] = result;

            return result;
        }

        _stemCache[cacheKey] = (null, null);

        return (null, null);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool IsNumber(string word, bool isHead)
    {
        // Quick check for any digit
        if ( !word.Any(char.IsDigit) )
        {
            return false;
        }

        // Check for valid number format with possible suffixes
        var end = word.Length;

        // Check for common suffixes
        foreach ( var suffix in PhonemizerConstants.Ordinals )
        {
            if ( EndsWith(word, suffix) )
            {
                end -= suffix.Length;

                break;
            }
        }

        if ( EndsWith(word, "'s") )
        {
            end -= 2;
        }
        else if ( EndsWith(word, "s") )
        {
            end -= 1;
        }
        else if ( EndsWith(word, "ing") )
        {
            end -= 3;
        }
        else if ( EndsWith(word, "'d") || EndsWith(word, "ed") )
        {
            end -= 2;
        }

        // Validate characters in the number portion
        for ( var i = 0; i < end; i++ )
        {
            var c = word[i];
            if ( !(char.IsDigit(c) || c == ',' || c == '.' || (isHead && i == 0 && c == '-')) )
            {
                return false;
            }
        }

        return true;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsCurrency(string word)
    {
        if ( !word.Contains('.') )
        {
            return true;
        }

        if ( word.Count(c => c == '.') > 1 )
        {
            return false;
        }

        var cents = word.Split('.')[1];

        return cents.Length < 3 || cents.All(c => c == '0');
    }

    private (string?, int?) GetNumber(string word, string? currency, bool isHead, string numFlags)
    {
        // Process suffix (like 'st', 'nd', 'rd', 'th')
        var suffixMatch = _suffixRegex.Match(word);
        var suffix      = suffixMatch.Success ? suffixMatch.Value : null;

        if ( suffix != null )
        {
            word = word[..^suffix.Length];
        }

        var result = new List<(string Phoneme, int Rating)>();

        // Handle negative numbers
        if ( word.StartsWith('-') )
        {
            ExtendResult("minus");
            word = word[1..];
        }

        // Main number processing logic
        if ( !isHead && !word.Contains('.') )
        {
            var num = word.Replace(",", "");

            // Handle leading zeros or longer numbers digit by digit
            if ( num[0] == '0' || num.Length > 3 )
            {
                foreach ( var digit in num )
                {
                    if ( !ExtendResult(digit.ToString(), false) )
                    {
                        return (null, null);
                    }
                }
            }
            // Handle 3-digit numbers that don't end with "00"
            else if ( num.Length == 3 && !num.EndsWith("00") )
            {
                // Handle first digit + "hundred"
                if ( !ExtendResult(num[0].ToString()) )
                {
                    return (null, null);
                }

                if ( !ExtendResult("hundred") )
                {
                    return (null, null);
                }

                // Handle remaining digits
                if ( num[1] == '0' )
                {
                    // Special case: x0y
                    if ( !ExtendResult("O", specialRating: -2) )
                    {
                        return (null, null);
                    }

                    if ( !ExtendResult(num[2].ToString(), false) )
                    {
                        return (null, null);
                    }
                }
                else
                {
                    // Handle last two digits as a number
                    if ( !ExtendNum(int.Parse(num.Substring(1, 2)), false) )
                    {
                        return (null, null);
                    }
                }
            }
            else
            {
                // Standard number conversion
                if ( !ExtendNum(int.Parse(num)) )
                {
                    return (null, null);
                }
            }
        }
        else if ( word.Count(c => c == '.') > 1 || !isHead )
        {
            // Handle multiple decimal points or non-head position
            var parts   = word.Replace(",", "").Split('.');
            var isFirst = true;

            foreach ( var part in parts )
            {
                if ( string.IsNullOrEmpty(part) )
                {
                    continue;
                }

                if ( part[0] == '0' || (part.Length != 2 && part.Any(n => n != '0')) )
                {
                    // Handle digit by digit
                    foreach ( var digit in part )
                    {
                        if ( !ExtendResult(digit.ToString(), false) )
                        {
                            return (null, null);
                        }
                    }
                }
                else
                {
                    // Standard number conversion
                    if ( !ExtendNum(int.Parse(part), isFirst) )
                    {
                        return (null, null);
                    }
                }

                isFirst = false;
            }
        }
        else if ( currency != null && PhonemizerConstants.Currencies.TryGetValue(currency, out var currencyPair) && IsCurrency(word) )
        {
            // Parse the parts
            var parts = word.Replace(",", "")
                            .Split('.')
                            .Select(p => string.IsNullOrEmpty(p) ? 0 : int.Parse(p))
                            .ToList();

            while ( parts.Count < 2 )
            {
                parts.Add(0);
            }

            // Filter out zero parts
            var nonZeroParts = parts.Take(2)
                                    .Select((value, index) => (Value: value, Unit: index == 0 ? currencyPair.Dollar : currencyPair.Cent))
                                    .Where(p => p.Value != 0)
                                    .ToList();

            for ( var i = 0; i < nonZeroParts.Count; i++ )
            {
                var (value, unit) = nonZeroParts[i];

                if ( i > 0 && !ExtendResult("and") )
                {
                    return (null, null);
                }

                // Convert number to words
                if ( !ExtendNum(value, i == 0) )
                {
                    return (null, null);
                }

                // Add currency unit
                if ( Math.Abs(value) != 1 && unit != "pence" )
                {
                    var (unitPhonemes, unitRating) = StemS(unit + "s", null, null, null);
                    if ( unitPhonemes != null )
                    {
                        result.Add((unitPhonemes, unitRating.Value));
                    }
                    else
                    {
                        return (null, null);
                    }
                }
                else
                {
                    if ( !ExtendResult(unit) )
                    {
                        return (null, null);
                    }
                }
            }
        }
        else
        {
            // Standard number handling for most cases
            string wordsForm;

            if ( int.TryParse(word.Replace(",", ""), out var number) )
            {
                // Choose conversion type
                var to = "cardinal";
                if ( suffix != null && PhonemizerConstants.Ordinals.Contains(suffix) )
                {
                    to = "ordinal";
                }
                else if ( result.Count == 0 && word.Length == 4 )
                {
                    to = "year";
                }

                wordsForm = Num2Words.Convert(number, to);
            }
            else if ( double.TryParse(word.Replace(",", ""), out var numberDouble) )
            {
                if ( word.StartsWith(".") )
                {
                    // Handle ".xx" format
                    wordsForm = "point " + string.Join(" ", word[1..].Select(c => Num2Words.Convert(int.Parse(c.ToString()))));
                }
                else
                {
                    wordsForm = Num2Words.Convert(numberDouble);
                }
            }
            else
            {
                return (null, null);
            }

            // Process the words form
            if ( !ExtendNum(wordsForm, escape: true) )
            {
                return (null, null);
            }
        }

        if ( result.Count == 0 )
        {
            return (null, null);
        }

        // Join the phonemes and handle suffix
        var combinedPhoneme = string.Join(" ", result.Select(r => r.Phoneme));
        var minRating       = result.Min(r => r.Rating);

        // Apply suffix transformations
        return suffix switch {
            "s" or "'s" => (S(combinedPhoneme), minRating),
            "ed" or "'d" => (Ed(combinedPhoneme), minRating),
            "ing" => (Ing(combinedPhoneme), minRating),
            _ => (combinedPhoneme, minRating)
        };

        // Helper method to extend number words
        bool ExtendNum(object num, bool first = true, bool escape = false)
        {
            var wordsForm = escape ? num.ToString()! : Num2Words.Convert(Convert.ToInt32(num));
            var splits    = wordsForm.Split(new[] { ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);

            for ( var i = 0; i < splits.Length; i++ )
            {
                var w = splits[i];

                if ( w != "and" || numFlags.Contains('&') )
                {
                    if ( first && i == 0 && splits.Length > 1 && w == "one" && numFlags.Contains('a') )
                    {
                        result.Add(("ə", 4));
                    }
                    else
                    {
                        double? specialRating = w == "point" ? -2.0 : null;
                        var (wordPhoneme, wordRating) = Lookup(w, null, specialRating, null);
                        if ( wordPhoneme != null )
                        {
                            result.Add((wordPhoneme, wordRating.Value));
                        }
                        else
                        {
                            return false;
                        }
                    }
                }
                else if ( w == "and" && numFlags.Contains('n') && result.Count > 0 )
                {
                    // Append "ən" to the previous word
                    var last = result[^1];
                    result[^1] = (last.Phoneme + "ən", last.Rating);
                }
            }

            return true;
        }

        // Helper method to add a single word result
        bool ExtendResult(string word, bool first = true, double? specialRating = null)
        {
            var (phoneme, rating) = Lookup(word, null, specialRating, null);
            if ( phoneme != null )
            {
                result.Add((phoneme, rating.Value));

                return true;
            }

            return false;
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private string AppendCurrency(string phonemes, string? currency)
    {
        if ( string.IsNullOrEmpty(currency) )
        {
            return phonemes;
        }

        if ( !PhonemizerConstants.Currencies.TryGetValue(currency, out var currencyPair) )
        {
            return phonemes;
        }

        var (stemPhonemes, _) = StemS(currencyPair.Dollar + "s", null, null, null);
        if ( stemPhonemes != null )
        {
            return $"{phonemes} {stemPhonemes}";
        }

        return phonemes;
    }

    [GeneratedRegex(@"(?i)vs\.?$", RegexOptions.Compiled, "en-BE")]
    private static partial Regex VersusRegex();

    [GeneratedRegex(@"([bcdgklmnprstvxz])\1ing$|cking$", RegexOptions.Compiled)]
    private static partial Regex DoubleConsonantIngRegex();

    [GeneratedRegex(@"[a-z']+$", RegexOptions.Compiled)]
    private static partial Regex SuffixRegex();
}

public class LruCache<TKey, TValue>(int capacity, IEqualityComparer<TKey> comparer)
    where TKey : notnull
{
    private readonly Dictionary<TKey, LinkedListNode<LruCacheItem>> _cacheMap = new(capacity, comparer);

    private readonly LinkedList<LruCacheItem> _lruList = new();

    public TValue this[TKey key]
    {
        get
        {
            if ( _cacheMap.TryGetValue(key, out var node) )
            {
                _lruList.Remove(node);
                _lruList.AddFirst(node);

                return node.Value.Value;
            }

            throw new KeyNotFoundException($"Key {key} not found in cache");
        }

        set
        {
            if ( _cacheMap.TryGetValue(key, out var existingNode) )
            {
                _lruList.Remove(existingNode);
                _lruList.AddFirst(existingNode);
                existingNode.Value.Value = value;

                return;
            }

            if ( _cacheMap.Count >= capacity )
            {
                var last = _lruList.Last;
                if ( last != null )
                {
                    _cacheMap.Remove(last.Value.Key);
                    _lruList.RemoveLast();
                }
            }

            var cacheItem = new LruCacheItem(key, value);
            var newNode   = new LinkedListNode<LruCacheItem>(cacheItem);
            _lruList.AddFirst(newNode);
            _cacheMap.Add(key, newNode);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryGetValue(TKey key, out TValue value)
    {
        if ( _cacheMap.TryGetValue(key, out var node) )
        {
            _lruList.Remove(node);
            _lruList.AddFirst(node);

            value = node.Value.Value;

            return true;
        }

        value = default!;

        return false;
    }

    public void Clear()
    {
        _cacheMap.Clear();
        _lruList.Clear();
    }

    private record LruCacheItem(TKey Key, TValue Value)
    {
        public TValue Value { get; set; } = Value;
    }
}

using System.Diagnostics;
using System.Drawing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

using Hexa.NET.ImGui;
using Hexa.NET.ImGui.Utilities;
using Hexa.NET.ImNodes;

using PersonaEngine.Lib.UI.Common;

using Silk.NET.Input;
using Silk.NET.Input.Extensions;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

using Shader = PersonaEngine.Lib.UI.Common.Shader;
using Texture = PersonaEngine.Lib.UI.Common.Texture;

namespace PersonaEngine.Lib.UI.GUI;

public class ImGuiController : IDisposable
{
    [FixedAddressValueType] private static SetClipboardDelegate setClipboardFn;

    [FixedAddressValueType] private static GetClipboardDelegate getClipboardFn;

    private readonly List<char> _pressedChars = new();

    private int _attribLocationProjMtx;

    private int _attribLocationTex;

    private int _attribLocationVtxColor;

    private int _attribLocationVtxPos;

    private int _attribLocationVtxUV;

    private IntPtr _clipboardTextPtr = IntPtr.Zero;

    private bool _ctrlVProcessed = false;

    private uint _elementsHandle;

    private Texture _fontTexture;

    private bool _frameBegun;

    private GL _gl;

    private IInputContext _input;

    private IKeyboard _keyboard;

    private ImGuiPlatformIOPtr _platform;

    private Shader _shader;

    private uint _vboHandle;

    private uint _vertexArrayObject;

    private IView _view;

    private bool _wasCtrlVPressed = false;

    private int _windowHeight;

    private int _windowWidth;

    public ImGuiContextPtr Context;

    /// <summary>
    ///     Constructs a new ImGuiController.
    /// </summary>
    public ImGuiController(GL gl, IView view, IInputContext input) : this(gl, view, input, null, null) { }

    /// <summary>
    ///     Constructs a new ImGuiController with font configuration.
    /// </summary>
    public ImGuiController(GL gl, IView view, IInputContext input, string primaryFontPath, string emojiFontPath) : this(gl, view, input, primaryFontPath, emojiFontPath, null) { }

    /// <summary>
    ///     Constructs a new ImGuiController with an onConfigureIO Action.
    /// </summary>
    public ImGuiController(GL gl, IView view, IInputContext input, Action? onConfigureIO) : this(gl, view, input, null, null, onConfigureIO) { }

    /// <summary>
    ///     Constructs a new ImGuiController with font configuration and onConfigure Action.
    /// </summary>
    public ImGuiController(GL gl, IView view, IInputContext input, string? primaryFontPath = null, string? emojiFontPath = null, Action? onConfigureIO = null)
    {
        Init(gl, view, input);

        var io          = ImGui.GetIO();
        var fontBuilder = new ImGuiFontBuilder();

        if ( primaryFontPath != null )
        {

                fontBuilder.AddFontFromFileTTF(primaryFontPath, 18f, [0x1, 0x1FFFF]);

        }
        else
        {
            fontBuilder.AddDefaultFont();
        }

        if ( emojiFontPath != null )
        {
            fontBuilder.SetOption(config =>
                                  {
                                      config.FontBuilderFlags |= (uint)ImGuiFreeTypeBuilderFlags.LoadColor;
                                      config.MergeMode        =  true;
                                      config.PixelSnapH       =  true;
                                  });

            fontBuilder.AddFontFromFileTTF(emojiFontPath, 14f, [0x1, 0x1FFFF]);
        }

        _ = fontBuilder.Build();
        io.Fonts.Build();

        io.BackendFlags                 |= ImGuiBackendFlags.RendererHasVtxOffset;
        io.ConfigFlags                  |= ImGuiConfigFlags.DockingEnable;
        io.ConfigViewportsNoAutoMerge   =  false;
        io.ConfigViewportsNoTaskBarIcon =  false;

        CreateDeviceResources();

        SetPerFrameImGuiData(1f / 60f);

        // Initialize ImNodes as you're already doing
        ImNodes.SetImGuiContext(Context);
        ImNodes.SetCurrentContext(ImNodes.CreateContext());
        ImNodes.StyleColorsDark(ImNodes.GetStyle());

        BeginFrame();
    }

    /// <summary>
    ///     Frees all graphics resources used by the renderer.
    /// </summary>
    public void Dispose()
    {
        _view.Resize      -= WindowResized;
        _keyboard.KeyChar -= OnKeyChar;

        _gl.DeleteBuffer(_vboHandle);
        _gl.DeleteBuffer(_elementsHandle);
        _gl.DeleteVertexArray(_vertexArrayObject);

        _fontTexture.Dispose();
        _shader.Dispose();

        ImGui.DestroyContext(Context);
    }

    public void MakeCurrent() { ImGui.SetCurrentContext(Context); }

    private unsafe void Init(GL gl, IView view, IInputContext input)
    {
        _gl           = gl;
        _view         = view;
        _input        = input;
        _windowWidth  = view.Size.X;
        _windowHeight = view.Size.Y;

        Context = ImGui.CreateContext();
        ImGui.SetCurrentContext(Context);
        ImGui.StyleColorsDark();

        _platform = ImGui.GetPlatformIO();
        var io = ImGui.GetIO();

        setClipboardFn = SetClipboard;
        getClipboardFn = GetClipboard;

        // Register clipboard handlers with both IO and PlatformIO
        _platform.PlatformSetClipboardTextFn = (void*)Marshal.GetFunctionPointerForDelegate(setClipboardFn);
        _platform.PlatformGetClipboardTextFn = (void*)Marshal.GetFunctionPointerForDelegate(getClipboardFn);
    }

    private void SetClipboard(IntPtr data)
    {
        Debug.WriteLine("SetClipboard called");

        if ( data == IntPtr.Zero )
        {
            return;
        }

        var text = Marshal.PtrToStringUTF8(data) ?? string.Empty;
        try
        {
            // Try the Silk.NET method first
            if ( _keyboard != null )
            {
                _keyboard.ClipboardText = text;
            }
            else
            {
                Debug.WriteLine("Keyboard reference is null when setting clipboard text");
            }
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Failed to set clipboard text via Silk.NET: {ex.Message}");
        }
    }

    private IntPtr GetClipboard()
    {
        Debug.WriteLine("GetClipboard called");

        if ( _clipboardTextPtr != IntPtr.Zero )
        {
            Marshal.FreeHGlobal(_clipboardTextPtr);
            _clipboardTextPtr = IntPtr.Zero;
        }

        var text    = string.Empty;
        var success = false;

        // Try primary method
        if ( _keyboard != null )
        {
            try
            {
                text    = _keyboard.ClipboardText ?? string.Empty;
                success = true;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Failed to get clipboard text via Silk.NET: {ex.Message}");
            }
        }

        // Convert to UTF-8 and allocate memory
        try
        {
            // Ensure null termination for C strings
            var bytes = Encoding.UTF8.GetBytes(text + '\0');
            _clipboardTextPtr = Marshal.AllocHGlobal(bytes.Length);
            Marshal.Copy(bytes, 0, _clipboardTextPtr, bytes.Length);

            return _clipboardTextPtr;
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Failed to allocate memory for clipboard text: {ex.Message}");

            return IntPtr.Zero;
        }
    }

    private void BeginFrame()
    {
        ImGui.NewFrame();
        _frameBegun       =  true;
        _keyboard         =  _input.Keyboards[0];
        _view.Resize      += WindowResized;
        _keyboard.KeyDown += OnKeyDown;
        _keyboard.KeyUp   += OnKeyUp;
        _keyboard.KeyChar += OnKeyChar;
    }

    private static void OnKeyDown(IKeyboard keyboard, Key keycode, int scancode) { OnKeyEvent(keyboard, keycode, scancode, true); }

    private static void OnKeyUp(IKeyboard keyboard, Key keycode, int scancode) { OnKeyEvent(keyboard, keycode, scancode, false); }

    private static void OnKeyEvent(IKeyboard keyboard, Key keycode, int scancode, bool down)
    {
        var io       = ImGui.GetIO();
        var imGuiKey = TranslateInputKeyToImGuiKey(keycode);
        io.AddKeyEvent(imGuiKey, down);
        io.SetKeyEventNativeData(imGuiKey, (int)keycode, scancode);
    }

    private void OnKeyChar(IKeyboard arg1, char arg2) { _pressedChars.Add(arg2); }

    private void WindowResized(Vector2D<int> size)
    {
        _windowWidth  = size.X;
        _windowHeight = size.Y;
    }

    /// <summary>
    ///     Renders the ImGui draw list data.
    ///     This method requires a <see cref="GraphicsDevice" /> because it may create new DeviceBuffers if the size of vertex
    ///     or index data has increased beyond the capacity of the existing buffers.
    ///     A <see cref="CommandList" /> is needed to submit drawing and resource update commands.
    /// </summary>
    public void Render()
    {
        if ( _frameBegun )
        {
            ImGui.End();

            var oldCtx = ImGui.GetCurrentContext();

            if ( oldCtx != Context )
            {
                ImGui.SetCurrentContext(Context);
            }

            _frameBegun = false;
            ImGui.Render();
            RenderImDrawData(ImGui.GetDrawData());

            if ( oldCtx != Context )
            {
                ImGui.SetCurrentContext(oldCtx);
            }
        }
    }

    /// <summary>
    ///     Updates ImGui input and IO configuration state.
    /// </summary>
    public void Update(float deltaSeconds)
    {
        var oldCtx = ImGui.GetCurrentContext();

        if ( oldCtx != Context )
        {
            ImGui.SetCurrentContext(Context);
        }

        if ( _frameBegun )
        {
            ImGui.Render();
        }

        SetPerFrameImGuiData(deltaSeconds);
        UpdateImGuiInput();
        SetDocking();

        _frameBegun = true;
    }

    private void SetDocking()
    {
        ImGui.NewFrame();
        var viewport = ImGui.GetMainViewport();

        var windowFlags = ImGuiWindowFlags.NoTitleBar |
                          ImGuiWindowFlags.NoResize |
                          ImGuiWindowFlags.NoMove |
                          ImGuiWindowFlags.NoBringToFrontOnFocus |
                          ImGuiWindowFlags.NoNavFocus;

        ImGui.SetNextWindowPos(viewport.Pos);
        ImGui.SetNextWindowSize(viewport.Size);
        ImGui.SetNextWindowViewport(viewport.ID);
        ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0.0f);
        ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0.0f);
        ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0));

        ImGui.Begin("DockSpace", windowFlags);
        ImGui.PopStyleVar(3);
        var dockspaceId = ImGui.GetID("MyDockSpace");
        ImGui.DockSpace(dockspaceId, new Vector2(0.0f, 0.0f), ImGuiDockNodeFlags.AutoHideTabBar);
    }

    /// <summary>
    ///     Sets per-frame data based on the associated window.
    ///     This is called by Update(float).
    /// </summary>
    private void SetPerFrameImGuiData(float deltaSeconds)
    {
        var io = ImGui.GetIO();
        io.DisplaySize = new Vector2(_windowWidth, _windowHeight);

        if ( _windowWidth > 0 && _windowHeight > 0 )
        {
            io.DisplayFramebufferScale = new Vector2(_view.FramebufferSize.X / _windowWidth,
                                                     _view.FramebufferSize.Y / _windowHeight);
        }

        io.DeltaTime = deltaSeconds; // DeltaTime is in seconds.
    }

    private void UpdateImGuiInput()
    {
        var io = ImGui.GetIO();

        using var mouseState = _input.Mice[0].CaptureState();

        io.MouseDown[0] = mouseState.IsButtonPressed(MouseButton.Left);
        io.MouseDown[1] = mouseState.IsButtonPressed(MouseButton.Right);
        io.MouseDown[2] = mouseState.IsButtonPressed(MouseButton.Middle);

        var point = new Point((int)mouseState.Position.X, (int)mouseState.Position.Y);
        io.MousePos = new Vector2(point.X, point.Y);

        var wheel = mouseState.GetScrollWheels()[0];
        io.MouseWheel  = wheel.Y;
        io.MouseWheelH = wheel.X;

        foreach ( var c in _pressedChars )
        {
            io.AddInputCharacter(c);
        }

        _pressedChars.Clear();

        io.KeyCtrl  = _keyboard.IsKeyPressed(Key.ControlLeft) || _keyboard.IsKeyPressed(Key.ControlRight);
        io.KeyAlt   = _keyboard.IsKeyPressed(Key.AltLeft) || _keyboard.IsKeyPressed(Key.AltRight);
        io.KeyShift = _keyboard.IsKeyPressed(Key.ShiftLeft) || _keyboard.IsKeyPressed(Key.ShiftRight);
        io.KeySuper = _keyboard.IsKeyPressed(Key.SuperLeft) || _keyboard.IsKeyPressed(Key.SuperRight);

        if ( io.KeyCtrl && _keyboard.IsKeyPressed(Key.C) )
        {
            io.AddKeyEvent(ImGuiKey.C, true);
            io.AddKeyEvent(ImGuiKey.LeftCtrl, true);

            Debug.WriteLine("Adding Ctrl+C event to ImGui");
        }

        var isCtrlVPressed = io.KeyCtrl && _keyboard.IsKeyPressed(Key.V);

        if ( isCtrlVPressed && !_wasCtrlVPressed )
        {
            _ctrlVProcessed = false;
        }

        if ( isCtrlVPressed && !_ctrlVProcessed )
        {
            io.AddKeyEvent(ImGuiKey.V, true);
            io.AddKeyEvent(ImGuiKey.LeftCtrl, true);

            var clipboardText = ImGui.GetClipboardTextS();
            if ( ImGui.IsAnyItemActive() )
            {
                foreach ( var c in clipboardText )
                {
                    io.AddInputCharacter(c);
                }
            }

            io.AddKeyEvent(ImGuiKey.V, false);
            io.AddKeyEvent(ImGuiKey.LeftCtrl, false);

            _ctrlVProcessed = true;
        }

        _wasCtrlVPressed = isCtrlVPressed;
    }

    internal void PressChar(char keyChar) { _pressedChars.Add(keyChar); }

    /// <summary>
    ///     Translates a Silk.NET.Input.Key to an ImGuiKey.
    /// </summary>
    /// <param name="key">The Silk.NET.Input.Key to translate.</param>
    /// <returns>The corresponding ImGuiKey.</returns>
    /// <exception cref="NotImplementedException">When the key has not been implemented yet.</exception>
    private static ImGuiKey TranslateInputKeyToImGuiKey(Key key)
    {
        return key switch {
            Key.Tab => ImGuiKey.Tab,
            Key.Left => ImGuiKey.LeftArrow,
            Key.Right => ImGuiKey.RightArrow,
            Key.Up => ImGuiKey.UpArrow,
            Key.Down => ImGuiKey.DownArrow,
            Key.PageUp => ImGuiKey.PageUp,
            Key.PageDown => ImGuiKey.PageDown,
            Key.Home => ImGuiKey.Home,
            Key.End => ImGuiKey.End,
            Key.Insert => ImGuiKey.Insert,
            Key.Delete => ImGuiKey.Delete,
            Key.Backspace => ImGuiKey.Backspace,
            Key.Space => ImGuiKey.Space,
            Key.Enter => ImGuiKey.Enter,
            Key.Escape => ImGuiKey.Escape,
            Key.Apostrophe => ImGuiKey.Apostrophe,
            Key.Comma => ImGuiKey.Comma,
            Key.Minus => ImGuiKey.Minus,
            Key.Period => ImGuiKey.Period,
            Key.Slash => ImGuiKey.Slash,
            Key.Semicolon => ImGuiKey.Semicolon,
            Key.Equal => ImGuiKey.Equal,
            Key.LeftBracket => ImGuiKey.LeftBracket,
            Key.BackSlash => ImGuiKey.Backslash,
            Key.RightBracket => ImGuiKey.RightBracket,
            Key.GraveAccent => ImGuiKey.GraveAccent,
            Key.CapsLock => ImGuiKey.CapsLock,
            Key.ScrollLock => ImGuiKey.ScrollLock,
            Key.NumLock => ImGuiKey.NumLock,
            Key.PrintScreen => ImGuiKey.PrintScreen,
            Key.Pause => ImGuiKey.Pause,
            Key.Keypad0 => ImGuiKey.Keypad0,
            Key.Keypad1 => ImGuiKey.Keypad1,
            Key.Keypad2 => ImGuiKey.Keypad2,
            Key.Keypad3 => ImGuiKey.Keypad3,
            Key.Keypad4 => ImGuiKey.Keypad4,
            Key.Keypad5 => ImGuiKey.Keypad5,
            Key.Keypad6 => ImGuiKey.Keypad6,
            Key.Keypad7 => ImGuiKey.Keypad7,
            Key.Keypad8 => ImGuiKey.Keypad8,
            Key.Keypad9 => ImGuiKey.Keypad9,
            Key.KeypadDecimal => ImGuiKey.KeypadDecimal,
            Key.KeypadDivide => ImGuiKey.KeypadDivide,
            Key.KeypadMultiply => ImGuiKey.KeypadMultiply,
            Key.KeypadSubtract => ImGuiKey.KeypadSubtract,
            Key.KeypadAdd => ImGuiKey.KeypadAdd,
            Key.KeypadEnter => ImGuiKey.KeypadEnter,
            Key.KeypadEqual => ImGuiKey.KeypadEqual,
            Key.ShiftLeft => ImGuiKey.LeftShift,
            Key.ControlLeft => ImGuiKey.LeftCtrl,
            Key.AltLeft => ImGuiKey.LeftAlt,
            Key.SuperLeft => ImGuiKey.LeftSuper,
            Key.ShiftRight => ImGuiKey.RightShift,
            Key.ControlRight => ImGuiKey.RightCtrl,
            Key.AltRight => ImGuiKey.RightAlt,
            Key.SuperRight => ImGuiKey.RightSuper,
            Key.Menu => ImGuiKey.Menu,
            Key.Number0 => ImGuiKey.Key0,
            Key.Number1 => ImGuiKey.Key1,
            Key.Number2 => ImGuiKey.Key2,
            Key.Number3 => ImGuiKey.Key3,
            Key.Number4 => ImGuiKey.Key4,
            Key.Number5 => ImGuiKey.Key5,
            Key.Number6 => ImGuiKey.Key6,
            Key.Number7 => ImGuiKey.Key7,
            Key.Number8 => ImGuiKey.Key8,
            Key.Number9 => ImGuiKey.Key9,
            Key.A => ImGuiKey.A,
            Key.B => ImGuiKey.B,
            Key.C => ImGuiKey.C,
            Key.D => ImGuiKey.D,
            Key.E => ImGuiKey.E,
            Key.F => ImGuiKey.F,
            Key.G => ImGuiKey.G,
            Key.H => ImGuiKey.H,
            Key.I => ImGuiKey.I,
            Key.J => ImGuiKey.J,
            Key.K => ImGuiKey.K,
            Key.L => ImGuiKey.L,
            Key.M => ImGuiKey.M,
            Key.N => ImGuiKey.N,
            Key.O => ImGuiKey.O,
            Key.P => ImGuiKey.P,
            Key.Q => ImGuiKey.Q,
            Key.R => ImGuiKey.R,
            Key.S => ImGuiKey.S,
            Key.T => ImGuiKey.T,
            Key.U => ImGuiKey.U,
            Key.V => ImGuiKey.V,
            Key.W => ImGuiKey.W,
            Key.X => ImGuiKey.X,
            Key.Y => ImGuiKey.Y,
            Key.Z => ImGuiKey.Z,
            Key.F1 => ImGuiKey.F1,
            Key.F2 => ImGuiKey.F2,
            Key.F3 => ImGuiKey.F3,
            Key.F4 => ImGuiKey.F4,
            Key.F5 => ImGuiKey.F5,
            Key.F6 => ImGuiKey.F6,
            Key.F7 => ImGuiKey.F7,
            Key.F8 => ImGuiKey.F8,
            Key.F9 => ImGuiKey.F9,
            Key.F10 => ImGuiKey.F10,
            Key.F11 => ImGuiKey.F11,
            Key.F12 => ImGuiKey.F12,
            Key.F13 => ImGuiKey.F13,
            Key.F14 => ImGuiKey.F14,
            Key.F15 => ImGuiKey.F15,
            Key.F16 => ImGuiKey.F16,
            Key.F17 => ImGuiKey.F17,
            Key.F18 => ImGuiKey.F18,
            Key.F19 => ImGuiKey.F19,
            Key.F20 => ImGuiKey.F20,
            Key.F21 => ImGuiKey.F21,
            Key.F22 => ImGuiKey.F22,
            Key.F23 => ImGuiKey.F23,
            Key.F24 => ImGuiKey.F24,
            _ => ImGuiKey.None
        };
    }

    private unsafe void SetupRenderState(ImDrawDataPtr drawDataPtr, int framebufferWidth, int framebufferHeight)
    {
        // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill
        _gl.Enable(GLEnum.Blend);
        _gl.BlendEquation(GLEnum.FuncAdd);
        _gl.BlendFuncSeparate(GLEnum.SrcAlpha, GLEnum.OneMinusSrcAlpha, GLEnum.One, GLEnum.OneMinusSrcAlpha);
        _gl.Disable(GLEnum.CullFace);
        _gl.Disable(GLEnum.DepthTest);
        _gl.Disable(GLEnum.StencilTest);
        _gl.Enable(GLEnum.ScissorTest);
#if !GLES && !LEGACY
        _gl.Disable(GLEnum.PrimitiveRestart);
        _gl.PolygonMode(GLEnum.FrontAndBack, GLEnum.Fill);
#endif

        var L = drawDataPtr.DisplayPos.X;
        var R = drawDataPtr.DisplayPos.X + drawDataPtr.DisplaySize.X;
        var T = drawDataPtr.DisplayPos.Y;
        var B = drawDataPtr.DisplayPos.Y + drawDataPtr.DisplaySize.Y;

        Span<float> orthoProjection = stackalloc float[] { 2.0f / (R - L), 0.0f, 0.0f, 0.0f, 0.0f, 2.0f / (T - B), 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, (R + L) / (L - R), (T + B) / (B - T), 0.0f, 1.0f };

        _shader.Use();
        _gl.Uniform1(_attribLocationTex, 0);
        _gl.UniformMatrix4(_attribLocationProjMtx, 1, false, orthoProjection);
        _gl.CheckError("Projection");

        _gl.BindSampler(0, 0);

        // Setup desired GL state
        // Recreate the VAO every time (this is to easily allow multiple GL contexts to be rendered to. VAO are not shared among GL contexts)
        // The renderer would actually work without any VAO bound, but then our VertexAttrib calls would overwrite the default one currently bound.
        _vertexArrayObject = _gl.GenVertexArray();
        _gl.BindVertexArray(_vertexArrayObject);
        _gl.CheckError("VAO");

        // Bind vertex/index buffers and setup attributes for ImDrawVert
        _gl.BindBuffer(GLEnum.ArrayBuffer, _vboHandle);
        _gl.BindBuffer(GLEnum.ElementArrayBuffer, _elementsHandle);
        _gl.EnableVertexAttribArray((uint)_attribLocationVtxPos);
        _gl.EnableVertexAttribArray((uint)_attribLocationVtxUV);
        _gl.EnableVertexAttribArray((uint)_attribLocationVtxColor);
        _gl.VertexAttribPointer((uint)_attribLocationVtxPos, 2, GLEnum.Float, false, (uint)sizeof(ImDrawVert), (void*)0);
        _gl.VertexAttribPointer((uint)_attribLocationVtxUV, 2, GLEnum.Float, false, (uint)sizeof(ImDrawVert), (void*)8);
        _gl.VertexAttribPointer((uint)_attribLocationVtxColor, 4, GLEnum.UnsignedByte, true, (uint)sizeof(ImDrawVert), (void*)16);
    }

    private unsafe void RenderImDrawData(ImDrawDataPtr drawDataPtr)
    {
        var framebufferWidth  = (int)(drawDataPtr.DisplaySize.X * drawDataPtr.FramebufferScale.X);
        var framebufferHeight = (int)(drawDataPtr.DisplaySize.Y * drawDataPtr.FramebufferScale.Y);
        if ( framebufferWidth <= 0 || framebufferHeight <= 0 )
        {
            return;
        }

        // Backup GL state
        _gl.GetInteger(GLEnum.ActiveTexture, out var lastActiveTexture);
        _gl.ActiveTexture(GLEnum.Texture0);

        _gl.GetInteger(GLEnum.CurrentProgram, out var lastProgram);
        _gl.GetInteger(GLEnum.TextureBinding2D, out var lastTexture);

        _gl.GetInteger(GLEnum.SamplerBinding, out var lastSampler);

        _gl.GetInteger(GLEnum.ArrayBufferBinding, out var lastArrayBuffer);
        _gl.GetInteger(GLEnum.VertexArrayBinding, out var lastVertexArrayObject);

#if !GLES
        Span<int> lastPolygonMode = stackalloc int[2];
        _gl.GetInteger(GLEnum.PolygonMode, lastPolygonMode);
#endif

        Span<int> lastScissorBox = stackalloc int[4];
        _gl.GetInteger(GLEnum.ScissorBox, lastScissorBox);

        _gl.GetInteger(GLEnum.BlendSrcRgb, out var lastBlendSrcRgb);
        _gl.GetInteger(GLEnum.BlendDstRgb, out var lastBlendDstRgb);

        _gl.GetInteger(GLEnum.BlendSrcAlpha, out var lastBlendSrcAlpha);
        _gl.GetInteger(GLEnum.BlendDstAlpha, out var lastBlendDstAlpha);

        _gl.GetInteger(GLEnum.BlendEquationRgb, out var lastBlendEquationRgb);
        _gl.GetInteger(GLEnum.BlendEquationAlpha, out var lastBlendEquationAlpha);

        var lastEnableBlend       = _gl.IsEnabled(GLEnum.Blend);
        var lastEnableCullFace    = _gl.IsEnabled(GLEnum.CullFace);
        var lastEnableDepthTest   = _gl.IsEnabled(GLEnum.DepthTest);
        var lastEnableStencilTest = _gl.IsEnabled(GLEnum.StencilTest);
        var lastEnableScissorTest = _gl.IsEnabled(GLEnum.ScissorTest);

#if !GLES && !LEGACY
        var lastEnablePrimitiveRestart = _gl.IsEnabled(GLEnum.PrimitiveRestart);
#endif

        SetupRenderState(drawDataPtr, framebufferWidth, framebufferHeight);

        // Will project scissor/clipping rectangles into framebuffer space
        var clipOff   = drawDataPtr.DisplayPos;       // (0,0) unless using multi-viewports
        var clipScale = drawDataPtr.FramebufferScale; // (1,1) unless using retina display which are often (2,2)

        // Render command lists
        for ( var n = 0; n < drawDataPtr.CmdListsCount; n++ )
        {
            var cmdListPtr = drawDataPtr.CmdLists[n];

            // Upload vertex/index buffers

            _gl.BufferData(GLEnum.ArrayBuffer, (nuint)(cmdListPtr.VtxBuffer.Size * sizeof(ImDrawVert)), cmdListPtr.VtxBuffer.Data, GLEnum.StreamDraw);
            _gl.CheckError($"Data Vert {n}");
            _gl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(cmdListPtr.IdxBuffer.Size * sizeof(ushort)), cmdListPtr.IdxBuffer.Data, GLEnum.StreamDraw);
            _gl.CheckError($"Data Idx {n}");

            for ( var cmd_i = 0; cmd_i < cmdListPtr.CmdBuffer.Size; cmd_i++ )
            {
                var cmdPtr = cmdListPtr.CmdBuffer[cmd_i];

                if ( cmdPtr.UserCallback != null )
                {
                    throw new NotImplementedException();
                }

                Vector4 clipRect;
                clipRect.X = (cmdPtr.ClipRect.X - clipOff.X) * clipScale.X;
                clipRect.Y = (cmdPtr.ClipRect.Y - clipOff.Y) * clipScale.Y;
                clipRect.Z = (cmdPtr.ClipRect.Z - clipOff.X) * clipScale.X;
                clipRect.W = (cmdPtr.ClipRect.W - clipOff.Y) * clipScale.Y;

                if ( clipRect.X < framebufferWidth && clipRect.Y < framebufferHeight && clipRect.Z >= 0.0f && clipRect.W >= 0.0f )
                {
                    // Apply scissor/clipping rectangle
                    _gl.Scissor((int)clipRect.X, (int)(framebufferHeight - clipRect.W), (uint)(clipRect.Z - clipRect.X), (uint)(clipRect.W - clipRect.Y));
                    _gl.CheckError("Scissor");

                    // Bind texture, Draw
                    _gl.BindTexture(GLEnum.Texture2D, (uint)cmdPtr.TextureId.Handle);
                    _gl.CheckError("Texture");

                    _gl.DrawElementsBaseVertex(GLEnum.Triangles, cmdPtr.ElemCount, GLEnum.UnsignedShort, (void*)(cmdPtr.IdxOffset * sizeof(ushort)), (int)cmdPtr.VtxOffset);
                    _gl.CheckError("Draw");
                }
            }
        }

        // Destroy the temporary VAO
        _gl.DeleteVertexArray(_vertexArrayObject);
        _vertexArrayObject = 0;

        // Restore modified GL state
        _gl.UseProgram((uint)lastProgram);
        _gl.BindTexture(GLEnum.Texture2D, (uint)lastTexture);

        _gl.BindSampler(0, (uint)lastSampler);

        _gl.ActiveTexture((GLEnum)lastActiveTexture);

        _gl.BindVertexArray((uint)lastVertexArrayObject);

        _gl.BindBuffer(GLEnum.ArrayBuffer, (uint)lastArrayBuffer);
        _gl.BlendEquationSeparate((GLEnum)lastBlendEquationRgb, (GLEnum)lastBlendEquationAlpha);
        _gl.BlendFuncSeparate((GLEnum)lastBlendSrcRgb, (GLEnum)lastBlendDstRgb, (GLEnum)lastBlendSrcAlpha, (GLEnum)lastBlendDstAlpha);

        if ( lastEnableBlend )
        {
            _gl.Enable(GLEnum.Blend);
        }
        else
        {
            _gl.Disable(GLEnum.Blend);
        }

        if ( lastEnableCullFace )
        {
            _gl.Enable(GLEnum.CullFace);
        }
        else
        {
            _gl.Disable(GLEnum.CullFace);
        }

        if ( lastEnableDepthTest )
        {
            _gl.Enable(GLEnum.DepthTest);
        }
        else
        {
            _gl.Disable(GLEnum.DepthTest);
        }

        if ( lastEnableStencilTest )
        {
            _gl.Enable(GLEnum.StencilTest);
        }
        else
        {
            _gl.Disable(GLEnum.StencilTest);
        }

        if ( lastEnableScissorTest )
        {
            _gl.Enable(GLEnum.ScissorTest);
        }
        else
        {
            _gl.Disable(GLEnum.ScissorTest);
        }

#if !GLES && !LEGACY
        if ( lastEnablePrimitiveRestart )
        {
            _gl.Enable(GLEnum.PrimitiveRestart);
        }
        else
        {
            _gl.Disable(GLEnum.PrimitiveRestart);
        }

        _gl.PolygonMode(GLEnum.FrontAndBack, (GLEnum)lastPolygonMode[0]);
#endif

        _gl.Scissor(lastScissorBox[0], lastScissorBox[1], (uint)lastScissorBox[2], (uint)lastScissorBox[3]);
    }

    private void CreateDeviceResources()
    {
        // Backup GL state

        _gl.GetInteger(GLEnum.TextureBinding2D, out var lastTexture);
        _gl.GetInteger(GLEnum.ArrayBufferBinding, out var lastArrayBuffer);
        _gl.GetInteger(GLEnum.VertexArrayBinding, out var lastVertexArray);

        var vertexSource =
            @"#version 330
        layout (location = 0) in vec2 Position;
        layout (location = 1) in vec2 UV;
        layout (location = 2) in vec4 Color;
        uniform mat4 ProjMtx;
        out vec2 Frag_UV;
        out vec4 Frag_Color;
        void main()
        {
            Frag_UV = UV;
            Frag_Color = Color;
            gl_Position = ProjMtx * vec4(Position.xy,0,1);
        }";

        var fragmentSource =
            @"#version 330
        in vec2 Frag_UV;
        in vec4 Frag_Color;
        uniform sampler2D Texture;
        layout (location = 0) out vec4 Out_Color;
        void main()
        {
            Out_Color = Frag_Color * texture(Texture, Frag_UV.st);
        }";

        _shader = new Shader(_gl, vertexSource, fragmentSource);

        _attribLocationTex      = _shader.GetUniformLocation("Texture");
        _attribLocationProjMtx  = _shader.GetUniformLocation("ProjMtx");
        _attribLocationVtxPos   = _shader.GetAttribLocation("Position");
        _attribLocationVtxUV    = _shader.GetAttribLocation("UV");
        _attribLocationVtxColor = _shader.GetAttribLocation("Color");

        _vboHandle      = _gl.GenBuffer();
        _elementsHandle = _gl.GenBuffer();

        RecreateFontDeviceTexture();

        // Restore modified GL state
        _gl.BindTexture(GLEnum.Texture2D, (uint)lastTexture);
        _gl.BindBuffer(GLEnum.ArrayBuffer, (uint)lastArrayBuffer);

        _gl.BindVertexArray((uint)lastVertexArray);

        _gl.CheckError("End of ImGui setup");
    }

    /// <summary>
    ///     Creates the texture used to render text.
    /// </summary>
    private unsafe void RecreateFontDeviceTexture()
    {
        // Build texture atlas
        var   io            = ImGui.GetIO();
        byte* pixels        = null;
        var   width         = 0;
        var   height        = 0;
        var   bytesPerPixel = 0;
        io.Fonts.GetTexDataAsRGBA32(ref pixels, ref width, ref height, ref bytesPerPixel); // Load as RGBA 32-bit (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory.

        // Upload texture to graphics system
        _gl.GetInteger(GLEnum.TextureBinding2D, out var lastTexture);

        _fontTexture = new Texture(_gl, width, height, new IntPtr(pixels));
        _fontTexture.Bind();
        _fontTexture.SetMagFilter(TextureMagFilter.Linear);
        _fontTexture.SetMinFilter(TextureMinFilter.Linear);

        // Store our identifier
        io.Fonts.SetTexID(new ImTextureID(_fontTexture.GlTexture));

        // Restore state
        _gl.BindTexture(GLEnum.Texture2D, (uint)lastTexture);
    }

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate void SetClipboardDelegate(IntPtr data);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate IntPtr GetClipboardDelegate();
}

using System.Buffers;

using PersonaEngine.Lib.Audio;

namespace PersonaEngine.Lib.Utils;

/// <summary>
///     Utility class for reading and writing wave files.
/// </summary>
internal class WaveFileUtils
{
    private static readonly byte[] ExpectedSubFormatForPcm = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71];

    public static async Task<HeaderParseResult> ParseHeaderAsync(Stream stream, CancellationToken cancellationToken)
    {
        var toReturn = new List<byte[]>();
        try
        {
            var headerChunks = new MergedMemoryChunks();

            // We load the first 44 bytes directly as we know that is the minimum size for a wave file header.
            if ( !await HaveEnoughDataAsync(headerChunks, 44, stream, toReturn, cancellationToken) )
            {
                return HeaderParseResult.WaitingForMoreData;
            }

            AudioSourceHeader? header = null;

            var chunkID = headerChunks.ReadUInt32LittleEndian();
            // Skip the file size
            headerChunks.TrySkip(4);
            var riffType = headerChunks.ReadUInt32LittleEndian();

            if ( chunkID != 0x46464952 /*'RIFF'*/ || riffType != 0x45564157 /*'WAVE'*/ )
            {
                return HeaderParseResult.Corrupt("Invalid wave file header.");
            }

            // Read chunks until we find 'fmt ' and 'data'
            while ( true )
            {
                if ( !await HaveEnoughDataAsync(headerChunks, 8, stream, toReturn, cancellationToken) )
                {
                    return HeaderParseResult.WaitingForMoreData;
                }

                var chunkType = headerChunks.ReadUInt32LittleEndian();
                var chunkSize = headerChunks.ReadUInt32LittleEndian();

                if ( chunkType == 0x20746D66 /*'fmt '*/ )
                {
                    if ( chunkSize < 16 )
                    {
                        return HeaderParseResult.Corrupt("Invalid wave file format chunk.");
                    }

                    if ( !await HaveEnoughDataAsync(headerChunks, chunkSize, stream, toReturn, cancellationToken) )
                    {
                        return HeaderParseResult.WaitingForMoreData;
                    }

                    var formatTag      = headerChunks.ReadUInt16LittleEndian();
                    var channels       = headerChunks.ReadUInt16LittleEndian();
                    var sampleRate     = headerChunks.ReadUInt32LittleEndian();
                    var avgBytesPerSec = headerChunks.ReadUInt32LittleEndian();
                    var blockAlign     = headerChunks.ReadUInt16LittleEndian();
                    var bitsPerSample  = headerChunks.ReadUInt16LittleEndian();

                    if ( formatTag != 1 && formatTag != 0xFFFE ) // PCM or WAVE_FORMAT_EXTENSIBLE
                    {
                        return HeaderParseResult.NotSupported("Unsupported wave file format.");
                    }

                    ushort? cbSize             = null;
                    ushort? validBitsPerSample = null;
                    uint?   channelMask        = null;
                    if ( formatTag == 0xFFFE )
                    {
                        // WAVE_FORMAT_EXTENSIBLE
                        if ( chunkSize < 40 )
                        {
                            return HeaderParseResult.Corrupt("Invalid wave file format chunk.");
                        }

                        cbSize             = headerChunks.ReadUInt16LittleEndian();
                        validBitsPerSample = headerChunks.ReadUInt16LittleEndian();
                        channelMask        = headerChunks.ReadUInt32LittleEndian();
                        var subFormatChunk = headerChunks.GetChunk(16);

                        if ( !subFormatChunk.Span.SequenceEqual(ExpectedSubFormatForPcm) )
                        {
                            return HeaderParseResult.NotSupported("Unsupported wave file format.");
                        }
                    }

                    if ( channels == 0 )
                    {
                        return HeaderParseResult.NotSupported("Cannot read wave file with 0 channels.");
                    }

                    // Skip any remaining bytes in fmt chunk
                    var remainingBytes = chunkSize - (formatTag == 1 ? 16u : 40u);
                    if ( !SkipData(headerChunks, remainingBytes, stream) )
                    {
                        return HeaderParseResult.WaitingForMoreData;
                    }

                    header = new AudioSourceHeader { Channels = channels, SampleRate = sampleRate, BitsPerSample = bitsPerSample };
                }
                else if ( chunkType == 0x61746164 /*'data'*/ )
                {
                    if ( header == null )
                    {
                        return HeaderParseResult.Corrupt("Data chunk found before format chunk.");
                    }

                    // Found 'data' chunk
                    // We can start processing samples after this point
                    var dataOffset = (int)(headerChunks.Position - headerChunks.AbsolutePositionOfCurrentChunk);

                    return HeaderParseResult.Success(header, dataOffset, chunkSize);
                }
                else
                {
                    if ( !SkipData(headerChunks, chunkSize, stream) )
                    {
                        return HeaderParseResult.WaitingForMoreData;
                    }
                }
            }
        }
        catch
        {
            foreach ( var rented in toReturn )
            {
                ArrayPool<byte>.Shared.Return(rented, ArrayPoolConfig.ClearOnReturn);
            }
        }

        return HeaderParseResult.WaitingForMoreData;
    }

    public static HeaderParseResult ParseHeader(MergedMemoryChunks headerChunks)
    {
        if ( headerChunks.Length < 12 )
        {
            return HeaderParseResult.WaitingForMoreData;
        }

        AudioSourceHeader? header = null;

        var chunkID = headerChunks.ReadUInt32LittleEndian();
        // Skip the file size
        headerChunks.TrySkip(4);
        var riffType = headerChunks.ReadUInt32LittleEndian();

        if ( chunkID != 0x46464952 /*'RIFF'*/ || riffType != 0x45564157 /*'WAVE'*/ )
        {
            return HeaderParseResult.Corrupt("Invalid wave file header.");
        }

        // Read chunks until we find 'fmt ' and 'data'
        while ( headerChunks.Position + 8 <= headerChunks.Length )
        {
            var chunkType = headerChunks.ReadUInt32LittleEndian();
            var chunkSize = headerChunks.ReadUInt32LittleEndian();

            if ( chunkType == 0x20746D66 /*'fmt '*/ )
            {
                if ( chunkSize < 16 )
                {
                    return HeaderParseResult.Corrupt("Invalid wave file format chunk.");
                }

                if ( headerChunks.Position + chunkSize > headerChunks.Length )
                {
                    return HeaderParseResult.WaitingForMoreData;
                }

                var formatTag  = headerChunks.ReadUInt16LittleEndian();
                var channels   = headerChunks.ReadUInt16LittleEndian();
                var sampleRate = headerChunks.ReadUInt32LittleEndian();
                headerChunks.TrySkip(6); // avgBytesPerSec + blockAlign

                var bitsPerSample = headerChunks.ReadUInt16LittleEndian();

                if ( formatTag != 1 && formatTag != 0xFFFE ) // PCM or WAVE_FORMAT_EXTENSIBLE
                {
                    return HeaderParseResult.NotSupported("Unsupported wave file format.");
                }

                if ( formatTag == 0xFFFE )
                {
                    // WAVE_FORMAT_EXTENSIBLE
                    if ( chunkSize < 40 )
                    {
                        return HeaderParseResult.Corrupt("Invalid wave file format chunk.");
                    }

                    headerChunks.TrySkip(8); // cbSize + validBitsPerSample + channelMask
                    var subFormatChunk = headerChunks.GetChunk(16);

                    if ( !subFormatChunk.Span.SequenceEqual(ExpectedSubFormatForPcm) )
                    {
                        return HeaderParseResult.NotSupported("Unsupported wave file format.");
                    }
                }

                if ( channels == 0 )
                {
                    return HeaderParseResult.NotSupported("Cannot read wave file with 0 channels.");
                }

                // Skip any remaining bytes in fmt chunk
                if ( !headerChunks.TrySkip(chunkSize - (formatTag == 1 ? 16u : 40u)) )
                {
                    return HeaderParseResult.WaitingForMoreData;
                }

                header = new AudioSourceHeader { Channels = channels, SampleRate = sampleRate, BitsPerSample = bitsPerSample };
            }
            else if ( chunkType == 0x61746164 /*'data'*/ )
            {
                if ( header == null )
                {
                    return HeaderParseResult.Corrupt("Data chunk found before format chunk.");
                }

                // Found 'data' chunk
                // We can start processing samples after this point
                var dataOffset = (int)(headerChunks.Position - headerChunks.AbsolutePositionOfCurrentChunk);

                return HeaderParseResult.Success(header, dataOffset, chunkSize);
            }
            else
            {
                if ( !headerChunks.TrySkip(chunkSize) )
                {
                    return HeaderParseResult.WaitingForMoreData;
                }
            }
        }

        return HeaderParseResult.WaitingForMoreData;
    }

    /// <summary>
    ///     Ensures that the given number of bytes can be read from the memory chunks.
    /// </summary>
    /// <returns></returns>
    private static async Task<bool> HaveEnoughDataAsync(MergedMemoryChunks headerChunks, uint requiredBytes, Stream stream, List<byte[]> returnItems, CancellationToken cancellationToken)
    {
        var extraBytesNeeded = (int)(requiredBytes - (headerChunks.Length - headerChunks.Position));
        if ( extraBytesNeeded <= 0 )
        {
            return true;
        }

        // We try to read the next chunk from the stream
        var nextChunk = ArrayPool<byte>.Shared.Rent(extraBytesNeeded);
        returnItems.Add(nextChunk);
        var chunkMemory = nextChunk.AsMemory(0, extraBytesNeeded);

        var actualReadNext = await stream.ReadAsync(chunkMemory, cancellationToken);

        if ( actualReadNext != extraBytesNeeded )
        {
            return false;
        }

        headerChunks.AddChunk(chunkMemory);

        return true;
    }

    private static bool SkipData(MergedMemoryChunks headerChunks, uint skipBytes, Stream stream)
    {
        var extraBytesNeededToSkip = (int)(skipBytes - (headerChunks.Length - headerChunks.Position));
        if ( extraBytesNeededToSkip <= 0 )
        {
            return headerChunks.TrySkip(skipBytes);
        }

        // We skip all the bytes we have in the current chunks + the remaining bytes from the stream directly
        if ( !headerChunks.TrySkip((uint)(headerChunks.Length - headerChunks.Position)) )
        {
            return false;
        }

        var skipped = stream.Seek(skipBytes, SeekOrigin.Current);

        return skipped == extraBytesNeededToSkip;
    }

    internal class HeaderParseResult(bool isSuccess, bool isIncomplete, bool isCorrupt, bool isNotSupported, int dataOffset, long dataChunkSize, AudioSourceHeader? header, string? errorMessage)
    {
        public static HeaderParseResult WaitingForMoreData { get; } = new(false, true, false, false, 0, 0, null, "Not enough data available.");

        public bool IsSuccess { get; } = isSuccess;

        public bool IsIncomplete { get; } = isIncomplete;

        public bool IsCorrupt { get; } = isCorrupt;

        public bool IsNotSupported { get; } = isNotSupported;

        public int DataOffset { get; } = dataOffset;

        public long DataChunkSize { get; } = dataChunkSize;

        public AudioSourceHeader? Header { get; } = header;

        public string? ErrorMessage { get; } = errorMessage;

        public static HeaderParseResult Corrupt(string message) { return new HeaderParseResult(true, false, true, true, 0, 0, null, message); }

        public static HeaderParseResult NotSupported(string message) { return new HeaderParseResult(false, false, false, true, 0, 0, null, message); }

        public static HeaderParseResult Success(AudioSourceHeader header, int dataOffset, long dataChunkSize) { return new HeaderParseResult(true, false, false, false, dataOffset, dataChunkSize, header, null); }
    }
}

using System.Text.Json;

using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

/// <summary>
///     モーションのクラス。
/// </summary>
public class CubismMotion : ACubismMotion
{
    public const string EffectNameEyeBlink = "EyeBlink";

    public const string EffectNameLipSync = "LipSync";

    public const string TargetNameModel = "Model";

    public const string TargetNameParameter = "Parameter";

    public const string TargetNamePartOpacity = "PartOpacity";

    // Id
    public const string IdNameOpacity = "Opacity";

    /// <summary>
    ///     mtnファイルで定義される一連のモーションの長さ
    /// </summary>
    private readonly float _loopDurationSeconds;

    /// <summary>
    ///     実際のモーションデータ本体
    /// </summary>
    private readonly CubismMotionData _motionData;

    /// <summary>
    ///     ロードしたファイルのFPS。記述が無ければデフォルト値15fpsとなる
    /// </summary>
    private readonly float _sourceFrameRate;

    /**
    * Cubism SDK R2 以前のモーションを再現させるなら true 、アニメータのモーションを正しく再現するなら false 。
    */
    private readonly bool UseOldBeziersCurveMotion = false;

    /// <summary>
    ///     自動まばたきを適用するパラメータIDハンドルのリスト。  モデル（モデルセッティング）とパラメータを対応付ける。
    /// </summary>
    private List<string> _eyeBlinkParameterIds;

    /// <summary>
    ///     最後に設定された重み
    /// </summary>
    private float _lastWeight;

    /// <summary>
    ///     リップシンクを適用するパラメータIDハンドルのリスト。  モデル（モデルセッティング）とパラメータを対応付ける。
    /// </summary>
    private List<string> _lipSyncParameterIds;

    /// <summary>
    ///     モデルが持つ自動まばたき用パラメータIDのハンドル。  モデルとモーションを対応付ける。
    /// </summary>
    private string _modelCurveIdEyeBlink;

    /// <summary>
    ///     モデルが持つリップシンク用パラメータIDのハンドル。  モデルとモーションを対応付ける。
    /// </summary>
    private string _modelCurveIdLipSync;

    /// <summary>
    ///     モデルが持つ不透明度用パラメータIDのハンドル。  モデルとモーションを対応付ける。
    /// </summary>
    private string _modelCurveIdOpacity;

    /// <summary>
    ///     モーションから取得した不透明度
    /// </summary>
    private float _modelOpacity;

    /// <summary>
    ///     インスタンスを作成する。
    /// </summary>
    /// <param name="buffer">motion3.jsonが読み込まれているバッファ</param>
    /// <param name="onFinishedMotionHandler">モーション再生終了時に呼び出されるコールバック関数。NULLの場合、呼び出されない。</param>
    public CubismMotion(string buffer, FinishedMotionCallback? onFinishedMotionHandler = null)
    {
        _sourceFrameRate     = 30.0f;
        _loopDurationSeconds = -1.0f;
        IsLoopFadeIn         = true; // ループ時にフェードインが有効かどうかのフラグ
        _modelOpacity        = 1.0f;

        using var stream = File.Open(buffer, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        var obj = JsonSerializer.Deserialize(stream, CubismMotionObjContext.Default.CubismMotionObj)
                  ?? throw new Exception("Load Motion error");

        _motionData = new CubismMotionData {
                                               Duration   = obj.Meta.Duration,
                                               Loop       = obj.Meta.Loop,
                                               CurveCount = obj.Meta.CurveCount,
                                               Fps        = obj.Meta.Fps,
                                               EventCount = obj.Meta.UserDataCount,
                                               Curves     = new CubismMotionCurve[obj.Meta.CurveCount],
                                               Segments   = new CubismMotionSegment[obj.Meta.TotalSegmentCount],
                                               Points     = new CubismMotionPoint[obj.Meta.TotalPointCount],
                                               Events     = new CubismMotionEvent[obj.Meta.UserDataCount]
                                           };

        var areBeziersRestructed = obj.Meta.AreBeziersRestricted;

        if ( obj.Meta.FadeInTime != null )
        {
            FadeInSeconds = obj.Meta.FadeInTime < 0.0f ? 1.0f : (float)obj.Meta.FadeInTime;
        }
        else
        {
            FadeInSeconds = 1.0f;
        }

        if ( obj.Meta.FadeOutTime != null )
        {
            FadeOutSeconds = obj.Meta.FadeOutTime < 0.0f ? 1.0f : (float)obj.Meta.FadeOutTime;
        }
        else
        {
            FadeOutSeconds = 1.0f;
        }

        var totalPointCount   = 0;
        var totalSegmentCount = 0;

        // Curves
        for ( var curveCount = 0; curveCount < _motionData.CurveCount; ++curveCount )
        {
            var item = obj.Curves[curveCount];
            var key  = item.Target;
            _motionData.Curves[curveCount] = new CubismMotionCurve {
                                                                       Id               = CubismFramework.CubismIdManager.GetId(item.Id),
                                                                       BaseSegmentIndex = totalSegmentCount,
                                                                       FadeInTime       = item.FadeInTime != null ? (float)item.FadeInTime : -1.0f,
                                                                       FadeOutTime      = item.FadeOutTime != null ? (float)item.FadeOutTime : -1.0f,
                                                                       Type = key switch {
                                                                           TargetNameModel => CubismMotionCurveTarget.Model,
                                                                           TargetNameParameter => CubismMotionCurveTarget.Parameter,
                                                                           TargetNamePartOpacity => CubismMotionCurveTarget.PartOpacity,
                                                                           _ => throw new Exception("Error: Unable to get segment type from Curve! The number of \"CurveCount\" may be incorrect!")
                                                                       }
                                                                   };

            // Segments
            for ( var segmentPosition = 0; segmentPosition < item.Segments.Count; )
            {
                if ( segmentPosition == 0 )
                {
                    _motionData.Segments[totalSegmentCount] = new CubismMotionSegment { BasePointIndex = totalPointCount };
                    _motionData.Points[totalPointCount]     = new CubismMotionPoint { Time             = item.Segments[segmentPosition], Value = item.Segments[segmentPosition + 1] };

                    totalPointCount += 1;
                    segmentPosition += 2;
                }
                else
                {
                    _motionData.Segments[totalSegmentCount] = new CubismMotionSegment { BasePointIndex = totalPointCount - 1 };
                }

                switch ( (CubismMotionSegmentType)item.Segments[segmentPosition] )
                {
                    case CubismMotionSegmentType.Linear:
                    {
                        _motionData.Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType.Linear;
                        _motionData.Segments[totalSegmentCount].Evaluate    = LinearEvaluate;

                        _motionData.Points[totalPointCount] = new CubismMotionPoint { Time = item.Segments[segmentPosition + 1], Value = item.Segments[segmentPosition + 2] };

                        totalPointCount += 1;
                        segmentPosition += 3;

                        break;
                    }
                    case CubismMotionSegmentType.Bezier:
                    {
                        _motionData.Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType.Bezier;
                        if ( areBeziersRestructed || UseOldBeziersCurveMotion )
                        {
                            _motionData.Segments[totalSegmentCount].Evaluate = BezierEvaluate;
                        }
                        else
                        {
                            _motionData.Segments[totalSegmentCount].Evaluate = BezierEvaluateCardanoInterpretation;
                        }

                        _motionData.Points[totalPointCount] = new CubismMotionPoint { Time = item.Segments[segmentPosition + 1], Value = item.Segments[segmentPosition + 2] };

                        _motionData.Points[totalPointCount + 1] = new CubismMotionPoint { Time = item.Segments[segmentPosition + 3], Value = item.Segments[segmentPosition + 4] };

                        _motionData.Points[totalPointCount + 2] = new CubismMotionPoint { Time = item.Segments[segmentPosition + 5], Value = item.Segments[segmentPosition + 6] };

                        totalPointCount += 3;
                        segmentPosition += 7;

                        break;
                    }
                    case CubismMotionSegmentType.Stepped:
                    {
                        _motionData.Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType.Stepped;
                        _motionData.Segments[totalSegmentCount].Evaluate    = SteppedEvaluate;

                        _motionData.Points[totalPointCount] = new CubismMotionPoint { Time = item.Segments[segmentPosition + 1], Value = item.Segments[segmentPosition + 2] };

                        totalPointCount += 1;
                        segmentPosition += 3;

                        break;
                    }
                    case CubismMotionSegmentType.InverseStepped:
                    {
                        _motionData.Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType.InverseStepped;
                        _motionData.Segments[totalSegmentCount].Evaluate    = InverseSteppedEvaluate;

                        _motionData.Points[totalPointCount] = new CubismMotionPoint { Time = item.Segments[segmentPosition + 1], Value = item.Segments[segmentPosition + 2] };

                        totalPointCount += 1;
                        segmentPosition += 3;

                        break;
                    }
                    default:
                    {
                        throw new Exception("CubismMotionSegmentType error");
                    }
                }

                ++_motionData.Curves[curveCount].SegmentCount;
                ++totalSegmentCount;
            }
        }

        for ( var userdatacount = 0; userdatacount < obj.Meta.UserDataCount; ++userdatacount )
        {
            _motionData.Events[userdatacount] = new CubismMotionEvent { FireTime = obj.UserData[userdatacount].Time, Value = obj.UserData[userdatacount].Value };
        }

        _sourceFrameRate     = _motionData.Fps;
        _loopDurationSeconds = _motionData.Duration;
        OnFinishedMotion     = onFinishedMotionHandler;
    }

    /// <summary>
    ///     ループするか?
    /// </summary>
    public bool IsLoop { get; set; }

    /// <summary>
    ///     ループ時にフェードインが有効かどうかのフラグ。初期値では有効。
    /// </summary>
    public bool IsLoopFadeIn { get; set; }

    private static CubismMotionPoint LerpPoints(CubismMotionPoint a, CubismMotionPoint b, float t) { return new CubismMotionPoint { Time = a.Time + (b.Time - a.Time) * t, Value = a.Value + (b.Value - a.Value) * t }; }

    private static float LinearEvaluate(CubismMotionPoint[] points, int start, float time)
    {
        var t = (time - points[start].Time) / (points[start + 1].Time - points[start].Time);

        if ( t < 0.0f )
        {
            t = 0.0f;
        }

        return points[start].Value + (points[start + 1].Value - points[start].Value) * t;
    }

    private static float BezierEvaluate(CubismMotionPoint[] points, int start, float time)
    {
        var t = (time - points[start].Time) / (points[start + 3].Time - points[start].Time);

        if ( t < 0.0f )
        {
            t = 0.0f;
        }

        var p01 = LerpPoints(points[start], points[start + 1], t);
        var p12 = LerpPoints(points[start + 1], points[start + 2], t);
        var p23 = LerpPoints(points[start + 2], points[start + 3], t);

        var p012 = LerpPoints(p01, p12, t);
        var p123 = LerpPoints(p12, p23, t);

        return LerpPoints(p012, p123, t).Value;
    }

    //private static float BezierEvaluateBinarySearch(List<CubismMotionPoint> points, int start, float time)
    //{
    //    float x_error = 0.01f;

    //    float x = time;
    //    float x1 = points[0].Time;
    //    float x2 = points[3].Time;
    //    float cx1 = points[1].Time;
    //    float cx2 = points[2].Time;

    //    float ta = 0.0f;
    //    float tb = 1.0f;
    //    float t = 0.0f;
    //    int i = 0;
    //    for (; i < 20; ++i)
    //    {
    //        if (x < x1 + x_error)
    //        {
    //            t = ta;
    //            break;
    //        }

    //        if (x2 - x_error < x)
    //        {
    //            t = tb;
    //            break;
    //        }

    //        float centerx = (cx1 + cx2) * 0.5f;
    //        cx1 = (x1 + cx1) * 0.5f;
    //        cx2 = (x2 + cx2) * 0.5f;
    //        float ctrlx12 = (cx1 + centerx) * 0.5f;
    //        float ctrlx21 = (cx2 + centerx) * 0.5f;
    //        centerx = (ctrlx12 + ctrlx21) * 0.5f;
    //        if (x < centerx)
    //        {
    //            tb = (ta + tb) * 0.5f;
    //            if (centerx - x_error < x)
    //            {
    //                t = tb;
    //                break;
    //            }

    //            x2 = centerx;
    //            cx2 = ctrlx12;
    //        }
    //        else
    //        {
    //            ta = (ta + tb) * 0.5f;
    //            if (x < centerx + x_error)
    //            {
    //                t = ta;
    //                break;
    //            }

    //            x1 = centerx;
    //            cx1 = ctrlx21;
    //        }
    //    }

    //    if (i == 20)
    //    {
    //        t = (ta + tb) * 0.5f;
    //    }

    //    if (t < 0.0f)
    //    {
    //        t = 0.0f;
    //    }
    //    if (t > 1.0f)
    //    {
    //        t = 1.0f;
    //    }

    //    CubismMotionPoint p01 = LerpPoints(points[start], points[start + 1], t);
    //    CubismMotionPoint p12 = LerpPoints(points[start + 1], points[start + 2], t);
    //    CubismMotionPoint p23 = LerpPoints(points[start + 2], points[start + 3], t);

    //    CubismMotionPoint p012 = LerpPoints(p01, p12, t);
    //    CubismMotionPoint p123 = LerpPoints(p12, p23, t);

    //    return LerpPoints(p012, p123, t).Value;
    //}

    private static float BezierEvaluateCardanoInterpretation(CubismMotionPoint[] points, int start, float time)
    {
        var x   = time;
        var x1  = points[start].Time;
        var x2  = points[start + 3].Time;
        var cx1 = points[start + 1].Time;
        var cx2 = points[start + 2].Time;

        var a = x2 - 3.0f * cx2 + 3.0f * cx1 - x1;
        var b = 3.0f * cx2 - 6.0f * cx1 + 3.0f * x1;
        var c = 3.0f * cx1 - 3.0f * x1;
        var d = x1 - x;

        var t = CubismMath.CardanoAlgorithmForBezier(a, b, c, d);

        var p01 = LerpPoints(points[start], points[start + 1], t);
        var p12 = LerpPoints(points[start + 1], points[start + 2], t);
        var p23 = LerpPoints(points[start + 2], points[start + 3], t);

        var p012 = LerpPoints(p01, p12, t);
        var p123 = LerpPoints(p12, p23, t);

        return LerpPoints(p012, p123, t).Value;
    }

    private static float SteppedEvaluate(CubismMotionPoint[] points, int start, float time) { return points[start].Value; }

    private static float InverseSteppedEvaluate(CubismMotionPoint[] points, int start, float time) { return points[start + 1].Value; }

    private static float EvaluateCurve(CubismMotionData motionData, int index, float time)
    {
        // Find segment to evaluate.
        var curve = motionData.Curves[index];

        var target            = -1;
        var totalSegmentCount = curve.BaseSegmentIndex + curve.SegmentCount;
        var pointPosition     = 0;
        for ( var i = curve.BaseSegmentIndex; i < totalSegmentCount; ++i )
        {
            // Get first point of next segment.
            pointPosition = motionData.Segments[i].BasePointIndex
                            + (motionData.Segments[i].SegmentType ==
                               CubismMotionSegmentType.Bezier
                                   ? 3
                                   : 1);

            // Break if time lies within current segment.
            if ( motionData.Points[pointPosition].Time > time )
            {
                target = i;

                break;
            }
        }

        if ( target == -1 )
        {
            return motionData.Points[pointPosition].Value;
        }

        var segment = motionData.Segments[target];

        return segment.Evaluate(motionData.Points, segment.BasePointIndex, time);
    }

    /// <summary>
    ///     モデルのパラメータ更新を実行する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="userTimeSeconds">現在の時刻[秒]</param>
    /// <param name="fadeWeight">モーションの重み</param>
    /// <param name="motionQueueEntry">CubismMotionQueueManagerで管理されているモーション</param>
    public override void DoUpdateParameters(CubismModel model, float userTimeSeconds, float fadeWeight, CubismMotionQueueEntry motionQueueEntry)
    {
        _modelCurveIdEyeBlink ??= CubismFramework.CubismIdManager.GetId(EffectNameEyeBlink);

        _modelCurveIdLipSync ??= CubismFramework.CubismIdManager.GetId(EffectNameLipSync);

        _modelCurveIdOpacity ??= CubismFramework.CubismIdManager.GetId(IdNameOpacity);

        var timeOffsetSeconds = userTimeSeconds - motionQueueEntry.StartTime;

        if ( timeOffsetSeconds < 0.0f )
        {
            timeOffsetSeconds = 0.0f; // エラー回避
        }

        var lipSyncValue  = float.MaxValue;
        var eyeBlinkValue = float.MaxValue;

        //まばたき、リップシンクのうちモーションの適用を検出するためのビット（maxFlagCount個まで
        var   MaxTargetSize = 64;
        ulong lipSyncFlags  = 0;
        ulong eyeBlinkFlags = 0;

        //瞬き、リップシンクのターゲット数が上限を超えている場合
        if ( _eyeBlinkParameterIds.Count > MaxTargetSize )
        {
            CubismLog.Warning($"[Live2D SDK]too many eye blink targets : {_eyeBlinkParameterIds.Count}");
        }

        if ( _lipSyncParameterIds.Count > MaxTargetSize )
        {
            CubismLog.Warning($"[Live2D SDK]too many lip sync targets : {_lipSyncParameterIds.Count}");
        }

        var tmpFadeIn = FadeInSeconds <= 0.0f ? 1.0f : CubismMath.GetEasingSine((userTimeSeconds - motionQueueEntry.FadeInStartTime) / FadeInSeconds);

        var tmpFadeOut = FadeOutSeconds <= 0.0f || motionQueueEntry.EndTime < 0.0f ? 1.0f : CubismMath.GetEasingSine((motionQueueEntry.EndTime - userTimeSeconds) / FadeOutSeconds);

        float value;
        int c,
            parameterIndex;

        // 'Repeat' time as necessary.
        var time = timeOffsetSeconds;

        if ( IsLoop )
        {
            while ( time > _motionData.Duration )
            {
                time -= _motionData.Duration;
            }
        }

        var curves = _motionData.Curves;

        // Evaluate model curves.
        for ( c = 0; c < _motionData.CurveCount && curves[c].Type == CubismMotionCurveTarget.Model; ++c )
        {
            // Evaluate curve and call handler.
            value = EvaluateCurve(_motionData, c, time);

            if ( curves[c].Id == _modelCurveIdEyeBlink )
            {
                eyeBlinkValue = value;
            }
            else if ( curves[c].Id == _modelCurveIdLipSync )
            {
                lipSyncValue = value;
            }
            else if ( curves[c].Id == _modelCurveIdOpacity )
            {
                _modelOpacity = value;

                // ------ 不透明度の値が存在すれば反映する ------
                model.SetModelOpacity(GetModelOpacityValue());
            }
        }

        var parameterMotionCurveCount = 0;

        for ( ; c < _motionData.CurveCount && curves[c].Type == CubismMotionCurveTarget.Parameter; ++c )
        {
            parameterMotionCurveCount++;

            // Find parameter index.
            parameterIndex = model.GetParameterIndex(curves[c].Id);

            // Skip curve evaluation if no value in sink.
            if ( parameterIndex == -1 )
            {
                continue;
            }

            var sourceValue = model.GetParameterValue(parameterIndex);

            // Evaluate curve and apply value.
            value = EvaluateCurve(_motionData, c, time);

            if ( eyeBlinkValue != float.MaxValue )
            {
                for ( var i = 0; i < _eyeBlinkParameterIds.Count && i < MaxTargetSize; ++i )
                {
                    if ( _eyeBlinkParameterIds[i] == curves[c].Id )
                    {
                        value         *= eyeBlinkValue;
                        eyeBlinkFlags |= 1UL << i;

                        break;
                    }
                }
            }

            if ( lipSyncValue != float.MaxValue )
            {
                for ( var i = 0; i < _lipSyncParameterIds.Count && i < MaxTargetSize; ++i )
                {
                    if ( _lipSyncParameterIds[i] == curves[c].Id )
                    {
                        value        += lipSyncValue;
                        lipSyncFlags |= 1UL << i;

                        break;
                    }
                }
            }

            float v;
            // パラメータごとのフェード
            if ( curves[c].FadeInTime < 0.0f && curves[c].FadeOutTime < 0.0f )
            {
                //モーションのフェードを適用
                v = sourceValue + (value - sourceValue) * fadeWeight;
            }
            else
            {
                // パラメータに対してフェードインかフェードアウトが設定してある場合はそちらを適用
                float fin;
                float fout;

                if ( curves[c].FadeInTime < 0.0f )
                {
                    fin = tmpFadeIn;
                }
                else
                {
                    fin = curves[c].FadeInTime == 0.0f
                              ? 1.0f
                              : CubismMath.GetEasingSine((userTimeSeconds - motionQueueEntry.FadeInStartTime) / curves[c].FadeInTime);
                }

                if ( curves[c].FadeOutTime < 0.0f )
                {
                    fout = tmpFadeOut;
                }
                else
                {
                    fout = curves[c].FadeOutTime == 0.0f || motionQueueEntry.EndTime < 0.0f
                               ? 1.0f
                               : CubismMath.GetEasingSine((motionQueueEntry.EndTime - userTimeSeconds) / curves[c].FadeOutTime);
                }

                var paramWeight = Weight * fin * fout;

                // パラメータごとのフェードを適用
                v = sourceValue + (value - sourceValue) * paramWeight;
            }

            model.SetParameterValue(parameterIndex, v);
        }

        if ( eyeBlinkValue != float.MaxValue )
        {
            for ( var i = 0; i < _eyeBlinkParameterIds.Count && i < MaxTargetSize; ++i )
            {
                var sourceValue = model.GetParameterValue(_eyeBlinkParameterIds[i]);
                //モーションでの上書きがあった時にはまばたきは適用しない
                if ( ((eyeBlinkFlags >> i) & 0x01) != 0UL )
                {
                    continue;
                }

                var v = sourceValue + (eyeBlinkValue - sourceValue) * fadeWeight;

                model.SetParameterValue(_eyeBlinkParameterIds[i], v);
            }
        }

        if ( lipSyncValue != float.MaxValue )
        {
            for ( var i = 0; i < _lipSyncParameterIds.Count && i < MaxTargetSize; ++i )
            {
                var sourceValue = model.GetParameterValue(_lipSyncParameterIds[i]);
                //モーションでの上書きがあった時にはリップシンクは適用しない
                if ( ((lipSyncFlags >> i) & 0x01) != 0UL )
                {
                    continue;
                }

                var v = sourceValue + (lipSyncValue - sourceValue) * fadeWeight;

                model.SetParameterValue(_lipSyncParameterIds[i], v);
            }
        }

        for ( ; c < _motionData.CurveCount && curves[c].Type == CubismMotionCurveTarget.PartOpacity; ++c )
        {
            // Find parameter index.
            parameterIndex = model.GetParameterIndex(curves[c].Id);

            // Skip curve evaluation if no value in sink.
            if ( parameterIndex == -1 )
            {
                continue;
            }

            // Evaluate curve and apply value.
            value = EvaluateCurve(_motionData, c, time);

            model.SetParameterValue(parameterIndex, value);
        }

        if ( timeOffsetSeconds >= _motionData.Duration )
        {
            if ( IsLoop )
            {
                motionQueueEntry.StartTime = userTimeSeconds; //最初の状態へ
                if ( IsLoopFadeIn )
                {
                    //ループ中でループ用フェードインが有効のときは、フェードイン設定し直し
                    motionQueueEntry.FadeInStartTime = userTimeSeconds;
                }
            }
            else
            {
                OnFinishedMotion?.Invoke(model, this);

                motionQueueEntry.Finished = true;
            }
        }

        _lastWeight = fadeWeight;
    }

    /// <summary>
    ///     モーションの長さを取得する。
    /// </summary>
    /// <returns>モーションの長さ[秒]</returns>
    public override float GetDuration() { return IsLoop ? -1.0f : _loopDurationSeconds; }

    /// <summary>
    ///     モーションのループ時の長さを取得する。
    /// </summary>
    /// <returns>モーションのループ時の長さ[秒]</returns>
    public override float GetLoopDuration() { return _loopDurationSeconds; }

    /// <summary>
    ///     パラメータに対するフェードインの時間を設定する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <param name="value">フェードインにかかる時間[秒]</param>
    public void SetParameterFadeInTime(string parameterId, float value)
    {
        var curves = _motionData.Curves;

        for ( var i = 0; i < _motionData.CurveCount; ++i )
        {
            if ( parameterId == curves[i].Id )
            {
                curves[i].FadeInTime = value;

                return;
            }
        }
    }

    /// <summary>
    ///     パラメータに対するフェードアウトの時間を設定する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <param name="value">フェードアウトにかかる時間[秒]</param>
    public void SetParameterFadeOutTime(string parameterId, float value)
    {
        var curves = _motionData.Curves;

        for ( var i = 0; i < _motionData.CurveCount; ++i )
        {
            if ( parameterId == curves[i].Id )
            {
                curves[i].FadeOutTime = value;

                return;
            }
        }
    }

    /// <summary>
    ///     パラメータに対するフェードインの時間を取得する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <returns>フェードインにかかる時間[秒]</returns>
    public float GetParameterFadeInTime(string parameterId)
    {
        var curves = _motionData.Curves;

        for ( var i = 0; i < _motionData.CurveCount; ++i )
        {
            if ( parameterId == curves[i].Id )
            {
                return curves[i].FadeInTime;
            }
        }

        return -1;
    }

    /// <summary>
    ///     パラメータに対するフェードアウトの時間を取得する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <returns>フェードアウトにかかる時間[秒]</returns>
    public float GetParameterFadeOutTime(string parameterId)
    {
        var curves = _motionData.Curves;

        for ( var i = 0; i < _motionData.CurveCount; ++i )
        {
            if ( parameterId == curves[i].Id )
            {
                return curves[i].FadeOutTime;
            }
        }

        return -1;
    }

    /// <summary>
    ///     自動エフェクトがかかっているパラメータIDリストを設定する。
    /// </summary>
    /// <param name="eyeBlinkParameterIds">自動まばたきがかかっているパラメータIDのリスト</param>
    /// <param name="lipSyncParameterIds">リップシンクがかかっているパラメータIDのリスト</param>
    public void SetEffectIds(List<string> eyeBlinkParameterIds, List<string> lipSyncParameterIds)
    {
        _eyeBlinkParameterIds = eyeBlinkParameterIds;
        _lipSyncParameterIds  = lipSyncParameterIds;
    }

    /// <summary>
    ///     イベント発火のチェック。
    ///     入力する時間は呼ばれるモーションタイミングを０とした秒数で行う。
    /// </summary>
    /// <param name="beforeCheckTimeSeconds">前回のイベントチェック時間[秒]</param>
    /// <param name="motionTimeSeconds">今回の再生時間[秒]</param>
    /// <returns></returns>
    public override List<string> GetFiredEvent(float beforeCheckTimeSeconds, float motionTimeSeconds)
    {
        FiredEventValues.Clear();
        /// イベントの発火チェック
        for ( var u = 0; u < _motionData.EventCount; ++u )
        {
            if ( _motionData.Events[u].FireTime > beforeCheckTimeSeconds &&
                 _motionData.Events[u].FireTime <= motionTimeSeconds )
            {
                FiredEventValues.Add(_motionData.Events[u].Value);
            }
        }

        return FiredEventValues;
    }

    /// <summary>
    ///     透明度のカーブが存在するかどうかを確認する
    /// </summary>
    /// <returns>
    ///     true  . キーが存在する
    ///     false . キーが存在しない
    /// </returns>
    public override bool IsExistModelOpacity()
    {
        for ( var i = 0; i < _motionData.CurveCount; i++ )
        {
            var curve = _motionData.Curves[i];

            if ( curve.Type != CubismMotionCurveTarget.Model )
            {
                continue;
            }

            if ( curve.Id == IdNameOpacity )
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    ///     透明度のカーブのインデックスを返す
    /// </summary>
    /// <returns>透明度のカーブのインデックス</returns>
    public override int GetModelOpacityIndex()
    {
        if ( IsExistModelOpacity() )
        {
            for ( var i = 0; i < _motionData.CurveCount; i++ )
            {
                var curve = _motionData.Curves[i];

                if ( curve.Type != CubismMotionCurveTarget.Model )
                {
                    continue;
                }

                if ( curve.Id == IdNameOpacity )
                {
                    return i;
                }
            }
        }

        return -1;
    }

    /// <summary>
    ///     透明度のIdを返す
    /// </summary>
    /// <returns>透明度のId</returns>
    public override string? GetModelOpacityId(int index)
    {
        if ( index != -1 )
        {
            var curve = _motionData.Curves[index];

            if ( curve.Type == CubismMotionCurveTarget.Model )
            {
                if ( curve.Id == IdNameOpacity )
                {
                    return CubismFramework.CubismIdManager.GetId(curve.Id);
                }
            }
        }

        return null;
    }

    /// <summary>
    ///     透明度の値を返す
    ///     更新後の値を取るにはUpdateParameters() の後に呼び出す。
    /// </summary>
    /// <returns>モーションの現在時間のOpacityの値</returns>
    public override float GetModelOpacityValue() { return _modelOpacity; }
}

using System.Text.Json;
using System.Text.Json.Serialization;

using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Implements centralized configuration management
/// </summary>
public class UiConfigurationManager : IUiConfigurationManager
{
    private readonly string _configFilePath;

    private readonly IOptionsMonitor<AvatarAppConfig> _configMonitor;

    private AvatarAppConfig _currentConfig;

    public UiConfigurationManager(
        IOptionsMonitor<AvatarAppConfig> configMonitor,
        string                           configFilePath = "appsettings.json")
    {
        _configMonitor  = configMonitor ?? throw new ArgumentNullException(nameof(configMonitor));
        _configFilePath = configFilePath;
        _currentConfig  = _configMonitor.CurrentValue;

        // Subscribe to configuration changes
        _configMonitor.OnChange(config =>
                                {
                                    _currentConfig = config;
                                    OnConfigurationChanged(null, ConfigurationChangedEventArgs.ChangeType.Reloaded);
                                });
    }

    public event EventHandler<ConfigurationChangedEventArgs> ConfigurationChanged;

    public T GetConfiguration<T>(string sectionKey = null)
    {
        if ( sectionKey == null )
        {
            if ( typeof(T) == typeof(AvatarAppConfig) )
            {
                return (T)(object)_currentConfig;
            }

            throw new ArgumentException($"Invalid configuration type {typeof(T).Name} without section key");
        }

        return sectionKey switch {
            "TTS" => (T)(object)_currentConfig.Tts,
            "Voice" => (T)(object)_currentConfig.Tts.Voice,
            "RouletteWheel" => (T)(object)_currentConfig.RouletteWheel,
            "Microphone" => (T)(object)_currentConfig.Microphone,
            _ => throw new ArgumentException($"Unknown section key: {sectionKey}")
        };
    }

    public void UpdateConfiguration<T>(T configuration, string? sectionKey = null)
    {
        if ( sectionKey == null )
        {
            if ( configuration is AvatarAppConfig appConfig )
            {
                _currentConfig = appConfig;
                OnConfigurationChanged(sectionKey, ConfigurationChangedEventArgs.ChangeType.Updated);

                return;
            }

            throw new ArgumentException($"Invalid configuration type {typeof(T).Name} without section key");
        }

        switch ( sectionKey )
        {
            case "TTS":
                if ( configuration is TtsConfiguration ttsConfig )
                {
                    _currentConfig = _currentConfig with { Tts = ttsConfig };
                    OnConfigurationChanged(sectionKey, ConfigurationChangedEventArgs.ChangeType.Updated);
                }
                else
                {
                    throw new ArgumentException($"Invalid configuration type {typeof(T).Name} for section {sectionKey}");
                }

                break;

            case "Voice":
                if ( configuration is KokoroVoiceOptions voiceOptions )
                {
                    var tts = _currentConfig.Tts;
                    _currentConfig = _currentConfig with { Tts = tts with { Voice = voiceOptions } };
                    OnConfigurationChanged(sectionKey, ConfigurationChangedEventArgs.ChangeType.Updated);
                }
                else
                {
                    throw new ArgumentException($"Invalid configuration type {typeof(T).Name} for section {sectionKey}");
                }

                break;

            case "Roulette":
                if ( configuration is RouletteWheelOptions rouletteWheelOptions )
                {
                    _currentConfig = _currentConfig with { RouletteWheel = rouletteWheelOptions };
                    OnConfigurationChanged(sectionKey, ConfigurationChangedEventArgs.ChangeType.Updated);
                }
                else
                {
                    throw new ArgumentException($"Invalid configuration type {typeof(T).Name} for section {sectionKey}");
                }

                break;
            case "Microphone":
                if ( configuration is MicrophoneConfiguration microphoneConfig )
                {
                    _currentConfig = _currentConfig with { Microphone = microphoneConfig };
                    OnConfigurationChanged(sectionKey, ConfigurationChangedEventArgs.ChangeType.Updated);
                }
                else
                {
                    throw new ArgumentException($"Invalid configuration type {typeof(T).Name} for section {sectionKey}");
                }

                break;
            default:
                throw new ArgumentException($"Unknown section key: {sectionKey}");
        }
    }

    public void SaveConfiguration()
    {
        // Save the configuration to the JSON file
        try
        {
            var jsonString = JsonSerializer.Serialize(
                                                      new Dictionary<string, object> { { "Config", _currentConfig } },
                                                      new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }
                                                     );

            File.WriteAllText(_configFilePath, jsonString);
            OnConfigurationChanged(null, ConfigurationChangedEventArgs.ChangeType.Saved);
        }
        catch (Exception ex)
        {
            // Convert to a more specific exception type with details
            throw new ConfigurationSaveException($"Failed to save configuration to {_configFilePath}", ex);
        }
    }

    public void ReloadConfiguration()
    {
        // Load the latest configuration from options monitor
        _currentConfig = _configMonitor.CurrentValue;
        OnConfigurationChanged(null, ConfigurationChangedEventArgs.ChangeType.Reloaded);
    }

    private void OnConfigurationChanged(string sectionKey, ConfigurationChangedEventArgs.ChangeType type) { ConfigurationChanged?.Invoke(this, new ConfigurationChangedEventArgs(sectionKey, type)); }
}

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
using PersonaEngine.Lib.Live2D.App;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Live2D.Behaviour.LipSync;

/// <summary>
///     LipSync service for VBridger parameter conventions.
/// </summary>
public sealed class VBridgerLipSyncService : ILive2DAnimationService
{
    public VBridgerLipSyncService(ILogger<VBridgerLipSyncService> logger, IAudioProgressNotifier audioProgressNotifier)
    {
        _logger                = logger;
        _audioProgressNotifier = audioProgressNotifier;
        _phonemeMap            = InitializeMisakiPhonemeMap();

        SubscribeToAudioProgressNotifier();
    }

    private void InitializeCurrentParameters()
    {
        if ( _model == null )
        {
            return;
        }

        var cubismModel = _model.Model;

        // Fetch initial values from the model
        _currentParameterValues[ParamMouthOpenY]        = cubismModel.GetParameterValue(ParamMouthOpenY);
        _currentParameterValues[ParamJawOpen]           = cubismModel.GetParameterValue(ParamJawOpen);
        _currentParameterValues[ParamMouthForm]         = cubismModel.GetParameterValue(ParamMouthForm);
        _currentParameterValues[ParamMouthShrug]        = cubismModel.GetParameterValue(ParamMouthShrug);
        _currentParameterValues[ParamMouthFunnel]       = cubismModel.GetParameterValue(ParamMouthFunnel);
        _currentParameterValues[ParamMouthPuckerWiden]  = cubismModel.GetParameterValue(ParamMouthPuckerWiden);
        _currentParameterValues[ParamMouthPressLipOpen] = cubismModel.GetParameterValue(ParamMouthPressLipOpen);
        _currentParameterValues[ParamMouthX]            = cubismModel.GetParameterValue(ParamMouthX);
        _currentParameterValues[ParamCheekPuffC]        = 0f;
    }

    #region Configuration Constants

    private const float SMOOTHING_FACTOR = 35.0f; // How quickly parameters move towards target (higher = faster)

    private const float NEUTRAL_RETURN_FACTOR = 15.0f; // How quickly parameters return to neutral when idle

    private const float CHEEK_PUFF_DECAY_FACTOR = 80.0f; // How quickly CheekPuff returns to 0

    private const float NEUTRAL_THRESHOLD = 0.02f; // Threshold for considering a value as neutral

    #endregion

    #region Parameter Names

    private static readonly string ParamMouthOpenY = "ParamMouthOpenY";

    private static readonly string ParamJawOpen = "ParamJawOpen";

    private static readonly string ParamMouthForm = "ParamMouthForm";

    private static readonly string ParamMouthShrug = "ParamMouthShrug";

    private static readonly string ParamMouthFunnel = "ParamMouthFunnel";

    private static readonly string ParamMouthPuckerWiden = "ParamMouthPuckerWiden";

    private static readonly string ParamMouthPressLipOpen = "ParamMouthPressLipOpen";

    private static readonly string ParamMouthX = "ParamMouthX";

    private static readonly string ParamCheekPuffC = "ParamCheekPuffC";

    #endregion

    #region Dependencies and State

    private LAppModel? _model;

    private readonly IAudioProgressNotifier _audioProgressNotifier;

    private readonly List<TimedPhoneme> _activePhonemes = new();

    private int _currentPhonemeIndex = -1;

    private bool _isPlaying = false;

    private bool _isStarted = false;

    private bool _disposed = false;

    private PhonemePose _currentTargetPose = PhonemePose.Neutral;

    private PhonemePose _nextTargetPose = PhonemePose.Neutral;

    private float _interpolationT = 0f;

    private readonly Dictionary<string, float> _currentParameterValues = new();

    private readonly Dictionary<string, PhonemePose> _phonemeMap;

    private readonly HashSet<char> _phonemeShapeIgnoreChars = ['ˈ', 'ˌ', 'ː']; // Ignore stress/length marks

    private readonly ILogger<VBridgerLipSyncService> _logger;

    #endregion

    #region ILipSyncService Implementation

    private void SubscribeToAudioProgressNotifier()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        UnsubscribeFromCurrentNotifier();

        _audioProgressNotifier.ChunkPlaybackStarted += HandleChunkStarted;
        _audioProgressNotifier.ChunkPlaybackEnded   += HandleChunkEnded;
        _audioProgressNotifier.PlaybackProgress     += HandleProgress;
    }

    private void UnsubscribeFromCurrentNotifier()
    {
        _audioProgressNotifier.ChunkPlaybackStarted -= HandleChunkStarted;
        _audioProgressNotifier.ChunkPlaybackEnded   -= HandleChunkEnded;
        _audioProgressNotifier.PlaybackProgress     -= HandleProgress;

        ResetState();
    }

    public void Start(LAppModel model)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        _model = model;

        InitializeCurrentParameters();

        _isStarted = true;

        _logger.LogInformation("Started lip syncing.");
    }

    public void Stop()
    {
        _isStarted = false;
        ResetState();
        _logger.LogInformation("Stopped lip syncing.");
    }

    #endregion

    #region Update Logic

    public void Update(float deltaTime)
    {
        if ( deltaTime <= 0.0f || _disposed || !_isStarted || _model == null )
        {
            return;
        }

        if ( !_isPlaying )
        {
            SmoothParametersToNeutral(deltaTime);

            return;
        }

        var easedT          = EaseInOutQuad(_interpolationT);
        var frameTargetPose = PhonemePose.Lerp(_currentTargetPose, _nextTargetPose, easedT);
        SmoothParametersToTarget(frameTargetPose, deltaTime);
        ApplySmoothedParameters();
    }

    private void UpdateTargetPoses(float currentTime)
    {
        if ( _model == null || !_isPlaying || _activePhonemes.Count == 0 )
        {
            if ( _currentTargetPose != PhonemePose.Neutral || _nextTargetPose != PhonemePose.Neutral )
            {
                _currentTargetPose = GetPoseFromCurrentValues();
                _nextTargetPose    = PhonemePose.Neutral;
                _interpolationT    = 0f;
                _logger.LogTrace("Targeting Neutral Pose.");
            }

            _currentPhonemeIndex = -1;

            return;
        }

        var foundIndex = FindPhonemeIndexAtTime(currentTime);

        if ( foundIndex != -1 )
        {
            if ( foundIndex != _currentPhonemeIndex )
            {
                _logger.LogTrace("Phoneme changed: {PhonemeIndex} -> {FoundIndex} ({Phoneme}) at T={CurrentTime:F3}", _currentPhonemeIndex, foundIndex, _activePhonemes[foundIndex].Phoneme, currentTime);

                if ( _currentPhonemeIndex == -1 )
                {
                    _currentTargetPose = GetPoseFromCurrentValues();
                }
                else
                {
                    _currentTargetPose = GetPoseForPhoneme(_activePhonemes[_currentPhonemeIndex].Phoneme);
                }

                _nextTargetPose      = GetPoseForPhoneme(_activePhonemes[foundIndex].Phoneme);
                _currentPhonemeIndex = foundIndex;
            }

            var currentPh = _activePhonemes[_currentPhonemeIndex];
            var duration  = currentPh.EndTime - currentPh.StartTime;
            _interpolationT = duration > 0.001
                                  ? (float)Math.Clamp((currentTime - currentPh.StartTime) / duration, 0.0, 1.0)
                                  : 1.0f;
        }
        else
        {
            if ( _currentTargetPose != PhonemePose.Neutral || _nextTargetPose != PhonemePose.Neutral )
            {
                _currentTargetPose = GetPoseFromCurrentValues();
                _nextTargetPose    = PhonemePose.Neutral;
                _interpolationT    = 0f;
                _logger.LogTrace("Targeting Neutral Pose (Gap/End).");
            }

            if ( _activePhonemes.Count > 0 &&
                 (currentTime < _activePhonemes.First().StartTime - 0.1 ||
                  currentTime > _activePhonemes.Last().EndTime + 0.1) )
            {
                _currentPhonemeIndex = -1;
            }
        }
    }

    private int FindPhonemeIndexAtTime(float currentTime)
    {
        if ( _currentPhonemeIndex >= 0 && _currentPhonemeIndex < _activePhonemes.Count )
        {
            var ph = _activePhonemes[_currentPhonemeIndex];
            if ( currentTime >= ph.StartTime && currentTime < ph.EndTime + 0.001 )
            {
                return _currentPhonemeIndex;
            }
        }

        var searchStartIndex = Math.Max(0, _currentPhonemeIndex - 1);
        for ( var i = searchStartIndex; i < _activePhonemes.Count; i++ )
        {
            var ph = _activePhonemes[i];
            if ( currentTime >= ph.StartTime && currentTime < ph.EndTime + 0.001 )
            {
                return i;
            }
        }

        if ( _activePhonemes.Count > 0 )
        {
            if ( currentTime < _activePhonemes[0].StartTime )
            {
                return -1;
            }

            if ( Math.Abs(currentTime - _activePhonemes.Last().EndTime) < 0.01 )
            {
                return _activePhonemes.Count - 1;
            }
        }

        return -1;
    }

    private PhonemePose GetPoseFromCurrentValues()
    {
        return new PhonemePose(
                               _currentParameterValues[ParamMouthOpenY],
                               _currentParameterValues[ParamJawOpen],
                               _currentParameterValues[ParamMouthForm],
                               _currentParameterValues[ParamMouthShrug],
                               _currentParameterValues[ParamMouthFunnel],
                               _currentParameterValues[ParamMouthPuckerWiden],
                               _currentParameterValues[ParamMouthPressLipOpen],
                               _currentParameterValues[ParamMouthX],
                               _currentParameterValues[ParamCheekPuffC]
                              );
    }

    #endregion

    #region Parameter Smoothing

    private void SmoothParametersToTarget(PhonemePose targetPose, float deltaTime)
    {
        var smoothFactor = SMOOTHING_FACTOR * deltaTime;

        _currentParameterValues[ParamMouthOpenY]        = Lerp(_currentParameterValues[ParamMouthOpenY], targetPose.MouthOpenY, smoothFactor);
        _currentParameterValues[ParamJawOpen]           = Lerp(_currentParameterValues[ParamJawOpen], targetPose.JawOpen, smoothFactor);
        _currentParameterValues[ParamMouthForm]         = Lerp(_currentParameterValues[ParamMouthForm], targetPose.MouthForm, smoothFactor);
        _currentParameterValues[ParamMouthShrug]        = Lerp(_currentParameterValues[ParamMouthShrug], targetPose.MouthShrug, smoothFactor);
        _currentParameterValues[ParamMouthFunnel]       = Lerp(_currentParameterValues[ParamMouthFunnel], targetPose.MouthFunnel, smoothFactor);
        _currentParameterValues[ParamMouthPuckerWiden]  = Lerp(_currentParameterValues[ParamMouthPuckerWiden], targetPose.MouthPuckerWiden, smoothFactor);
        _currentParameterValues[ParamMouthPressLipOpen] = Lerp(_currentParameterValues[ParamMouthPressLipOpen], targetPose.MouthPressLipOpen, smoothFactor);
        _currentParameterValues[ParamMouthX]            = Lerp(_currentParameterValues[ParamMouthX], targetPose.MouthX, smoothFactor);

        if ( targetPose.CheekPuffC > NEUTRAL_THRESHOLD )
        {
            _currentParameterValues[ParamCheekPuffC] = Lerp(_currentParameterValues[ParamCheekPuffC], targetPose.CheekPuffC, smoothFactor);
        }
        else
        {
            var decayFactor = CHEEK_PUFF_DECAY_FACTOR * deltaTime;
            _currentParameterValues[ParamCheekPuffC] = Lerp(_currentParameterValues[ParamCheekPuffC], 0.0f, decayFactor);
        }
    }

    private void SmoothParametersToNeutral(float deltaTime)
    {
        var smoothFactor = NEUTRAL_RETURN_FACTOR * deltaTime;
        var neutral      = PhonemePose.Neutral;

        _currentParameterValues[ParamMouthOpenY]        = Lerp(_currentParameterValues[ParamMouthOpenY], neutral.MouthOpenY, smoothFactor);
        _currentParameterValues[ParamJawOpen]           = Lerp(_currentParameterValues[ParamJawOpen], neutral.JawOpen, smoothFactor);
        _currentParameterValues[ParamMouthForm]         = Lerp(_currentParameterValues[ParamMouthForm], neutral.MouthForm, smoothFactor);
        _currentParameterValues[ParamMouthShrug]        = Lerp(_currentParameterValues[ParamMouthShrug], neutral.MouthShrug, smoothFactor);
        _currentParameterValues[ParamMouthFunnel]       = Lerp(_currentParameterValues[ParamMouthFunnel], neutral.MouthFunnel, smoothFactor);
        _currentParameterValues[ParamMouthPuckerWiden]  = Lerp(_currentParameterValues[ParamMouthPuckerWiden], neutral.MouthPuckerWiden, smoothFactor);
        _currentParameterValues[ParamMouthPressLipOpen] = Lerp(_currentParameterValues[ParamMouthPressLipOpen], neutral.MouthPressLipOpen, smoothFactor);
        _currentParameterValues[ParamMouthX]            = Lerp(_currentParameterValues[ParamMouthX], neutral.MouthX, smoothFactor);

        var decayFactor = CHEEK_PUFF_DECAY_FACTOR * deltaTime;
        _currentParameterValues[ParamCheekPuffC] = Lerp(_currentParameterValues[ParamCheekPuffC], 0.0f, decayFactor);

        ApplySmoothedParameters();
    }

    private bool IsApproximatelyNeutral()
    {
        var neutral = PhonemePose.Neutral;

        return Math.Abs(_currentParameterValues[ParamMouthOpenY] - neutral.MouthOpenY) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamJawOpen] - neutral.JawOpen) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamMouthForm] - neutral.MouthForm) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamMouthShrug] - neutral.MouthShrug) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamMouthFunnel] - neutral.MouthFunnel) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamMouthPuckerWiden] - neutral.MouthPuckerWiden) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamMouthPressLipOpen] - neutral.MouthPressLipOpen) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamMouthX] - neutral.MouthX) < NEUTRAL_THRESHOLD &&
               Math.Abs(_currentParameterValues[ParamCheekPuffC] - neutral.CheekPuffC) < NEUTRAL_THRESHOLD;
    }

    private void ApplySmoothedParameters()
    {
        if ( _model == null )
        {
            _logger.LogWarning("Attempted to apply parameters but model is null.");

            return;
        }

        try
        {
            var cubismModel = _model.Model;
            cubismModel.SetParameterValue(ParamMouthOpenY, _currentParameterValues[ParamMouthOpenY]);
            cubismModel.SetParameterValue(ParamJawOpen, _currentParameterValues[ParamJawOpen]);
            cubismModel.SetParameterValue(ParamMouthForm, _currentParameterValues[ParamMouthForm]);
            cubismModel.SetParameterValue(ParamMouthShrug, _currentParameterValues[ParamMouthShrug]);
            cubismModel.SetParameterValue(ParamMouthFunnel, _currentParameterValues[ParamMouthFunnel]);
            cubismModel.SetParameterValue(ParamMouthPuckerWiden, _currentParameterValues[ParamMouthPuckerWiden]);
            cubismModel.SetParameterValue(ParamMouthPressLipOpen, _currentParameterValues[ParamMouthPressLipOpen]);
            cubismModel.SetParameterValue(ParamMouthX, _currentParameterValues[ParamMouthX]);
            cubismModel.SetParameterValue(ParamCheekPuffC, _currentParameterValues[ParamCheekPuffC]);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error applying Live2D parameters");
        }
    }

    #endregion

    #region Event Handlers and State Management

    private void HandleChunkStarted(object? sender, AudioChunkPlaybackStartedEvent e)
    {
        if ( !_isStarted )
        {
            return;
        }

        _logger.LogTrace("Audio Chunk Playback Started.");
        ProcessAudioSegment(e.Chunk);
        _isPlaying           = true;
        _currentPhonemeIndex = -1;

        var initialTime = _activePhonemes.Count != 0 ? (float)_activePhonemes.First().StartTime : 0f;
        UpdateTargetPoses(initialTime);
    }

    private void HandleChunkEnded(object? sender, AudioChunkPlaybackEndedEvent e)
    {
        if ( !_isStarted )
        {
            return;
        }

        _logger.LogTrace("Audio Chunk Playback Ended.");
        _isPlaying = false;
    }

    private void HandleProgress(object? sender, AudioPlaybackProgressEvent e)
    {
        if ( !_isStarted || !_isPlaying )
        {
            return;
        }

        UpdateTargetPoses((float)e.CurrentPlaybackTime.TotalSeconds);
    }

    private void ResetState()
    {
        _activePhonemes.Clear();
        _currentPhonemeIndex = -1;
        _isPlaying           = false;
        _currentTargetPose   = PhonemePose.Neutral;
        _nextTargetPose      = PhonemePose.Neutral;
        _interpolationT      = 0f;
        InitializeCurrentParameters();
        _logger.LogTrace("LipSync state reset.");
    }

    #endregion

    #region Phoneme Processing

    private void ProcessAudioSegment(AudioSegment segment)
    {
        _activePhonemes.Clear();

        var lastEndTime = 0.0;

        foreach ( var token in segment.Tokens )
        {
            if ( string.IsNullOrEmpty(token.Phonemes) || !token.StartTs.HasValue || !token.EndTs.HasValue || token.EndTs.Value <= token.StartTs.Value )
            {
                _logger.LogTrace("Skipping invalid token: Phonemes='{Phonemes}', Start={StartTs}, End={EndTs}", token.Phonemes, token.StartTs, token.EndTs);

                continue;
            }

            var tokenStartTime = Math.Max(lastEndTime, token.StartTs.Value);
            var tokenEndTime   = Math.Max(tokenStartTime + 0.001, token.EndTs.Value);

            var phonemeChars = SplitPhonemes(token.Phonemes);
            if ( phonemeChars.Count == 0 )
            {
                _logger.LogTrace("No valid phoneme characters found in token: '{Phonemes}'", token.Phonemes);

                continue;
            }

            var tokenDuration           = tokenEndTime - tokenStartTime;
            var timePerPhoneme          = tokenDuration / phonemeChars.Count;
            var currentPhonemeStartTime = tokenStartTime;

            foreach ( var phChar in phonemeChars )
            {
                var phonemeEndTime = currentPhonemeStartTime + timePerPhoneme;
                _activePhonemes.Add(new TimedPhoneme { Phoneme = phChar, StartTime = currentPhonemeStartTime, EndTime = phonemeEndTime });
                currentPhonemeStartTime = phonemeEndTime;
            }

            lastEndTime = currentPhonemeStartTime;
        }

        _activePhonemes.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
        _logger.LogDebug("Processed segment into {Count} timed phonemes.", _activePhonemes.Count);
    }

    private List<string> SplitPhonemes(string phonemeString)
    {
        var result = new List<string>();
        if ( string.IsNullOrEmpty(phonemeString) )
        {
            return result;
        }

        foreach ( var c in phonemeString )
        {
            if ( !_phonemeShapeIgnoreChars.Contains(c) )
            {
                result.Add(c.ToString());
            }
        }

        return result;
    }

    private PhonemePose GetPoseForPhoneme(string phoneme)
    {
        if ( _phonemeMap.TryGetValue(phoneme, out var pose) )
        {
            return pose;
        }

        _logger.LogDebug("Phoneme '{Phoneme}' not found in map. Returning Neutral.", phoneme);

        return PhonemePose.Neutral;
    }

    #endregion

    #region Helper Functions

    private static float EaseInOutQuad(float t)
    {
        t = Math.Clamp(t, 0.0f, 1.0f);

        return t < 0.5f ? 2.0f * t * t : 1.0f - (float)Math.Pow(-2.0 * t + 2.0, 2.0) / 2.0f;
    }

    private static float Lerp(float a, float b, float t)
    {
        t = Math.Clamp(t, 0.0f, 1.0f);

        return a + (b - a) * t;
    }

    #endregion

    #region Structs and Maps

    public struct PhonemePose : IEquatable<PhonemePose>
    {
        public float MouthOpenY; // 0-1: How open the mouth is

        public float JawOpen; // 0-1: How open the jaw is

        public float MouthForm; // -1 (Frown) to +1 (Smile): Vertical lip corner movement

        public float MouthShrug; // 0-1: Upward lip shrug/tension

        public float MouthFunnel; // 0-1: Funnel shape (lips forward/pursed)

        public float MouthPuckerWiden; // -1 (Wide) to +1 (Pucker): Controls mouth width

        public float MouthPressLipOpen; // -1 (Pressed Thin) to 0 (Touching) to +1 (Separated/Teeth)

        public float MouthX; // -1 to 1: Horizontal mouth shift

        public float CheekPuffC; // 0-1: Cheek puff amount

        public static PhonemePose Neutral = new();

        public PhonemePose(float openY = 0, float jawOpen = 0, float form = 0, float shrug = 0, float funnel = 0, float puckerWiden = 0, float pressLip = 0, float mouthX = 0, float cheekPuff = 0)
        {
            MouthOpenY        = openY;
            JawOpen           = jawOpen;
            MouthForm         = form;
            MouthShrug        = shrug;
            MouthFunnel       = funnel;
            MouthPuckerWiden  = puckerWiden;
            MouthPressLipOpen = pressLip;
            MouthX            = mouthX;
            CheekPuffC        = cheekPuff;
        }

        public static PhonemePose Lerp(PhonemePose a, PhonemePose b, float t)
        {
            t = Math.Clamp(t, 0.0f, 1.0f);

            return new PhonemePose(
                                   VBridgerLipSyncService.Lerp(a.MouthOpenY, b.MouthOpenY, t),
                                   VBridgerLipSyncService.Lerp(a.JawOpen, b.JawOpen, t),
                                   VBridgerLipSyncService.Lerp(a.MouthForm, b.MouthForm, t),
                                   VBridgerLipSyncService.Lerp(a.MouthShrug, b.MouthShrug, t),
                                   VBridgerLipSyncService.Lerp(a.MouthFunnel, b.MouthFunnel, t),
                                   VBridgerLipSyncService.Lerp(a.MouthPuckerWiden, b.MouthPuckerWiden, t),
                                   VBridgerLipSyncService.Lerp(a.MouthPressLipOpen, b.MouthPressLipOpen, t),
                                   VBridgerLipSyncService.Lerp(a.MouthX, b.MouthX, t),
                                   VBridgerLipSyncService.Lerp(a.CheekPuffC, b.CheekPuffC, t)
                                  );
        }

        public bool Equals(PhonemePose other)
        {
            const float tolerance = 0.001f;

            return Math.Abs(MouthOpenY - other.MouthOpenY) < tolerance &&
                   Math.Abs(JawOpen - other.JawOpen) < tolerance &&
                   Math.Abs(MouthForm - other.MouthForm) < tolerance &&
                   Math.Abs(MouthShrug - other.MouthShrug) < tolerance &&
                   Math.Abs(MouthFunnel - other.MouthFunnel) < tolerance &&
                   Math.Abs(MouthPuckerWiden - other.MouthPuckerWiden) < tolerance &&
                   Math.Abs(MouthPressLipOpen - other.MouthPressLipOpen) < tolerance &&
                   Math.Abs(MouthX - other.MouthX) < tolerance &&
                   Math.Abs(CheekPuffC - other.CheekPuffC) < tolerance;
        }

        public override bool Equals(object? obj) { return obj is PhonemePose other && Equals(other); }

        public override int GetHashCode() { return HashCode.Combine(HashCode.Combine(MouthOpenY, JawOpen, MouthForm, MouthShrug, MouthFunnel, MouthPuckerWiden, MouthPressLipOpen, MouthX), CheekPuffC); }

        public static bool operator ==(PhonemePose left, PhonemePose right) { return left.Equals(right); }

        public static bool operator !=(PhonemePose left, PhonemePose right) { return !(left == right); }
    }

    private struct TimedPhoneme
    {
        public string Phoneme;

        public double StartTime;

        public double EndTime;
    }

    private Dictionary<string, PhonemePose> InitializeMisakiPhonemeMap()
    {
        // Phoneme to pose mapping based on VBridger parameter definitions:
        // MouthForm: -1 (Frown) to +1 (Smile)
        // MouthPuckerWiden: -1 (Wide) to +1 (Pucker)
        // MouthPressLipOpen: -1 (Pressed Thin) to +1 (Separated/Teeth)

        var map = new Dictionary<string, PhonemePose>();

        // --- Neutral ---
        map.Add("SIL", PhonemePose.Neutral); // Neutral: open=0, jaw=0, form=0, shrug=0, funnel=0, puckerWiden=0, pressLip=0, puff=0

        // --- Shared IPA Consonants ---
        // Plosives (b, p, d, t, g, k) - Focus on closure and puff
        map.Add("b", new PhonemePose(pressLip: -1.0f, cheekPuff: 0.6f));              // Pressed lips, puff
        map.Add("p", new PhonemePose(pressLip: -1.0f, cheekPuff: 0.8f));              // Pressed lips, strong puff
        map.Add("d", new PhonemePose(0.05f, 0.05f, pressLip: 0.0f, cheekPuff: 0.2f)); // Slight open, lips touch/nearly touch, slight puff
        map.Add("t", new PhonemePose(0.05f, 0.05f, pressLip: 0.0f, cheekPuff: 0.3f)); // Slight open, lips touch/nearly touch, moderate puff
        map.Add("ɡ", new PhonemePose(0.1f, 0.15f, pressLip: 0.2f, cheekPuff: 0.5f));  // Back sound, slight open, slight separation, puff
        map.Add("k", new PhonemePose(0.1f, 0.15f, pressLip: 0.2f, cheekPuff: 0.4f));  // Back sound, slight open, slight separation, moderate puff

        // Fricatives (f, v, s, z, h, ʃ, ʒ, ð, θ) - Focus on partial closure/airflow shapes
        map.Add("f", new PhonemePose(0.05f, pressLip: -0.2f, form: -0.2f, puckerWiden: -0.1f));       // Lower lip near upper teeth: Slight press, slight frown, slightly wide
        map.Add("v", new PhonemePose(0.05f, pressLip: -0.1f, form: -0.2f, puckerWiden: -0.1f));       // Voiced 'f': Less press?
        map.Add("s", new PhonemePose(jawOpen: 0.0f, pressLip: 0.9f, form: 0.3f, puckerWiden: -0.6f)); // Teeth close/showing, slight smile, wide
        map.Add("z", new PhonemePose(jawOpen: 0.0f, pressLip: 0.8f, form: 0.2f, puckerWiden: -0.5f)); // Voiced 's': Slightly less extreme?
        map.Add("h", new PhonemePose(0.2f, 0.2f, pressLip: 0.5f));                                    // Breathy open, lips separated
        map.Add("ʃ", new PhonemePose(0.1f, funnel: 0.9f, puckerWiden: 0.6f, pressLip: 0.2f));         // 'sh': Funnel, puckered but flatter than 'oo', slight separation
        map.Add("ʒ", new PhonemePose(0.1f, funnel: 0.8f, puckerWiden: 0.5f, pressLip: 0.2f));         // 'zh': Similar to 'sh'
        map.Add("ð", new PhonemePose(0.05f, pressLip: 0.1f, puckerWiden: -0.2f));                     // Soft 'th': Tongue tip, lips nearly touching, slightly wide
        map.Add("θ", new PhonemePose(0.05f, pressLip: 0.2f, puckerWiden: -0.3f));                     // Hard 'th': More airflow? More separation/width?

        // Nasals (m, n, ŋ) - Focus on closure or near-closure
        map.Add("m", new PhonemePose(pressLip: -1.0f));              // Pressed lips
        map.Add("n", new PhonemePose(0.05f, 0.05f, pressLip: 0.0f)); // Like 'd' position, lips touching
        map.Add("ŋ", new PhonemePose(0.15f, 0.2f, pressLip: 0.4f));  // 'ng': Back tongue, mouth open more, lips separated

        // Liquids/Glides (l, ɹ, w, j) - Varied shapes
        map.Add("l", new PhonemePose(0.2f, 0.2f, puckerWiden: -0.3f, pressLip: 0.6f));                // Tongue tip visible: Slightly open, slightly wide, lips separated
        map.Add("ɹ", new PhonemePose(0.15f, 0.15f, funnel: 0.4f, puckerWiden: 0.2f, pressLip: 0.3f)); // 'r': Some funneling, slight pucker, separation
        map.Add("w", new PhonemePose(0.1f, 0.1f, funnel: 1.0f, puckerWiden: 0.9f, pressLip: -0.3f));  // Like 'u': Strong funnel, strong pucker, lips maybe slightly pressed
        map.Add("j", new PhonemePose(0.1f, 0.1f, 0.6f, 0.3f, puckerWiden: -0.8f, pressLip: 0.8f));    // 'y': Like 'i', smile, shrug, wide, teeth showing

        // --- Shared Consonant Clusters ---
        map.Add("ʤ", new PhonemePose(0.1f, funnel: 0.8f, puckerWiden: 0.5f, pressLip: 0.2f, cheekPuff: 0.3f)); // 'j'/'dg': Target 'ʒ' shape + puff
        map.Add("ʧ", new PhonemePose(0.1f, funnel: 0.9f, puckerWiden: 0.6f, pressLip: 0.2f, cheekPuff: 0.4f)); // 'ch': Target 'ʃ' shape + puff

        // --- Shared IPA Vowels ---
        map.Add("ə", new PhonemePose(0.3f, 0.3f, pressLip: 0.5f));                                     // Schwa: Neutral open, lips separated
        map.Add("i", new PhonemePose(0.1f, 0.1f, 0.7f, 0.4f, puckerWiden: -0.9f, pressLip: 0.9f));     // 'ee': Smile, shrug, wide, teeth showing
        map.Add("u", new PhonemePose(0.15f, 0.15f, funnel: 1.0f, puckerWiden: 1.0f, pressLip: -0.2f)); // 'oo': Funnel, puckered, slight press
        map.Add("ɑ", new PhonemePose(0.9f, 1.0f, pressLip: 0.8f));                                     // 'aa': Very open, lips separated
        map.Add("ɔ", new PhonemePose(0.6f, 0.7f, funnel: 0.5f, puckerWiden: 0.3f, pressLip: 0.7f));    // 'aw': Open, some funnel, slight pucker, separated
        map.Add("ɛ", new PhonemePose(0.5f, 0.5f, puckerWiden: -0.5f, pressLip: 0.7f));                 // 'eh': Mid open, somewhat wide, separated
        map.Add("ɜ", new PhonemePose(0.4f, 0.4f, pressLip: 0.6f));                                     // 'er': Mid open, neutral width, separated (blend with 'ɹ')
        map.Add("ɪ", new PhonemePose(0.2f, 0.2f, 0.2f, puckerWiden: -0.6f, pressLip: 0.8f));           // 'ih': Slight open, slight smile, wide, separated
        map.Add("ʊ", new PhonemePose(0.2f, 0.2f, funnel: 0.8f, puckerWiden: 0.7f, pressLip: 0.1f));    // 'uu': Slight open, funnel, pucker, near touch
        map.Add("ʌ", new PhonemePose(0.6f, 0.6f, pressLip: 0.7f));                                     // 'uh': Mid open, neutral width, separated

        // --- Shared Diphthong Vowels (Targeting the end-shape's characteristics) ---
        map.Add("A", new PhonemePose(0.3f, 0.3f, 0.4f, puckerWiden: -0.7f, pressLip: 0.8f));        // 'ay' (ends like ɪ/i): Mid-close, smile, wide, separated
        map.Add("I", new PhonemePose(0.4f, 0.4f, 0.3f, puckerWiden: -0.6f, pressLip: 0.8f));        // 'eye' (ends like ɪ/i): Mid-open, smile, wide, separated
        map.Add("W", new PhonemePose(0.3f, 0.3f, funnel: 0.9f, puckerWiden: 0.8f, pressLip: 0.0f)); // 'ow' (ends like ʊ/u): Mid-close, funnel, pucker, touching
        map.Add("Y", new PhonemePose(0.3f, 0.3f, 0.2f, puckerWiden: -0.5f, pressLip: 0.8f));        // 'oy' (ends like ɪ/i): Mid-close, smile, wide, separated

        // --- Shared Custom Vowel ---
        map.Add("ᵊ", new PhonemePose(0.1f, 0.1f, pressLip: 0.2f)); // Small schwa: Minimal open, slight separation

        // --- American-only ---
        map.Add("æ", new PhonemePose(0.7f, 0.7f, 0.3f, puckerWiden: -0.8f, pressLip: 0.9f));        // 'ae': Open, slight smile, wide, teeth showing
        map.Add("O", new PhonemePose(0.3f, 0.3f, funnel: 0.8f, puckerWiden: 0.6f, pressLip: 0.1f)); // US 'oh' (ends like ʊ/u): Mid-close, funnel, pucker, near touch
        map.Add("ᵻ", new PhonemePose(0.15f, 0.15f, puckerWiden: -0.2f, pressLip: 0.6f));            // Between ə/ɪ: Slightly open, neutral-wide, separated
        map.Add("ɾ", new PhonemePose(0.05f, 0.05f, pressLip: 0.3f));                                // Flap 't': Very quick, slight separation

        // --- British-only ---
        map.Add("a", new PhonemePose(0.7f, 0.7f, puckerWiden: -0.4f, pressLip: 0.8f));              // UK 'ash': Open, less wide than US 'æ', separated
        map.Add("Q", new PhonemePose(0.3f, 0.3f, funnel: 0.7f, puckerWiden: 0.5f, pressLip: 0.1f)); // UK 'oh' (ends like ʊ/u): Mid-close, funnel, pucker, near touch
        map.Add("ɒ", new PhonemePose(0.8f, 0.9f, funnel: 0.2f, puckerWiden: 0.1f, pressLip: 0.8f)); // 'on': Open, slight funnel, slight pucker, separated

        return map;
    }

    #endregion

    #region IDisposable Implementation

    public void Dispose() { Dispose(true); }

    private void Dispose(bool disposing)
    {
        if ( _disposed )
        {
            return;
        }

        if ( disposing )
        {
            _logger.LogDebug("Disposing...");
            Stop();
            UnsubscribeFromCurrentNotifier();
            _activePhonemes.Clear();
            _currentParameterValues.Clear();
        }

        _disposed = true;
        _logger.LogInformation("Disposed");
    }

    #endregion
}

using System.Numerics;

using Hexa.NET.ImGui;

using OpenAI.Chat;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;

namespace PersonaEngine.Lib.UI.GUI;

public class ChatEditor : ConfigSectionEditorBase
{
    private readonly FontProvider _fontProvider;

    private readonly IConversationOrchestrator _orchestrator;

    private readonly float _participantsPanelWidth = 200f;

    private readonly IUiThemeManager _themeManager;

    private IConversationContext? _currentContext;

    private string _editMessageContent = string.Empty;

    private Guid _editMessageId = Guid.Empty;

    private Guid _editTurnId = Guid.Empty;

    private IReadOnlyList<InteractionTurn> _historySnapshot = Array.Empty<InteractionTurn>();

    private bool _needsRedraw = true;

    private IReadOnlyDictionary<string, ParticipantInfo> _participantsSnapshot = new Dictionary<string, ParticipantInfo>();

    private InteractionTurn? _pendingTurnSnapshot;

    private bool _showConfirmClearPopup = false;

    private bool _showEditPopup = false;

    public ChatEditor(
        IUiConfigurationManager   configManager,
        IEditorStateManager       stateManager,
        IUiThemeManager           themeManager,
        IConversationOrchestrator orchestrator,
        FontProvider              fontProvider)
        : base(configManager, stateManager)
    {
        _orchestrator = orchestrator;
        _themeManager = themeManager;
        _fontProvider = fontProvider;

        _orchestrator.SessionsUpdated += OnCurrentContextChanged;
        OnCurrentContextChanged(null, EventArgs.Empty);
    }

    public override string SectionKey => "Chat";

    public override string DisplayName => "Conversation";

    private void OnCurrentContextChanged(object? sender, EventArgs e)
    {
        var sessionId = _orchestrator.GetActiveSessionIds().FirstOrDefault();
        if ( sessionId == Guid.Empty )
        {
            return;
        }

        var context = _orchestrator.GetSession(sessionId).Context;
        SetCurrentContext(context);
    }

    private void SetCurrentContext(IConversationContext? newContext)
    {
        if ( _currentContext == newContext )
        {
            return;
        }

        if ( _currentContext != null )
        {
            _currentContext.ConversationUpdated -= OnConversationUpdated;
        }

        _currentContext = newContext;

        if ( _currentContext != null )
        {
            _currentContext.ConversationUpdated += OnConversationUpdated;
            UpdateSnapshots();
        }
        else
        {
            _historySnapshot      = Array.Empty<InteractionTurn>();
            _participantsSnapshot = new Dictionary<string, ParticipantInfo>();
            _pendingTurnSnapshot  = null;
        }

        _needsRedraw = true;
    }

    private void OnConversationUpdated(object? sender, EventArgs e)
    {
        UpdateSnapshots();
        _needsRedraw = true;
    }

    private void UpdateSnapshots()
    {
        if ( _currentContext == null )
        {
            return;
        }

        _participantsSnapshot = new Dictionary<string, ParticipantInfo>(_currentContext.Participants);
        _historySnapshot      = _currentContext.History.Select(turn => turn.CreateSnapshot()).ToList();
        _pendingTurnSnapshot  = _currentContext.PendingTurn;
    }

    public override void Render()
    {
        if ( _needsRedraw )
        {
            _needsRedraw = false;
        }

        var availWidth = ImGui.GetContentRegionAvail().X;
        if ( _currentContext == null )
        {
            ImGui.TextWrapped("No active conversation selected.");

            return;
        }

        // --- Left Panel: Participants and Controls ---
        ImGui.BeginChild("ParticipantsPanel", new Vector2(_participantsPanelWidth, 700), ImGuiChildFlags.Borders);
        {
            RenderParticipantsPanel(_currentContext);
            RenderControlsPanel(_currentContext);
        }

        ImGui.EndChild();

        ImGui.SameLine();

        // --- Right Panel: Chat History ---
        var chatAreaWidth = availWidth - _participantsPanelWidth - ImGui.GetStyle().ItemSpacing.X;
        ImGui.BeginChild("ChatHistoryPanel", new Vector2(chatAreaWidth, 700), ImGuiChildFlags.None);
        {
            RenderChatHistory(_currentContext, chatAreaWidth);
        }

        ImGui.EndChild();

        // --- Popups ---
        RenderEditPopup(_currentContext);
        RenderConfirmClearPopup(_currentContext);
    }

    private void RenderParticipantsPanel(IConversationContext context)
    {
        ImGui.Text("Participants");
        ImGui.Separator();

        if ( !_participantsSnapshot.Any() )
        {
            ImGui.TextDisabled("No participants.");

            return;
        }

        foreach ( var participant in _participantsSnapshot.Values )
        {
            // Simple display: Icon (optional) + Name + Role
            ImGui.BulletText($"{participant.Name} ({participant.Role})");
            // Optional: Add context menu to remove participant?
            // if (ImGui.BeginPopupContextItem($"participant_{participant.Id}")) { ... }
        }
    }

    private void RenderControlsPanel(IConversationContext context)
    {
        ImGui.Separator();
        ImGui.Spacing();
        ImGui.Text("Controls");
        ImGui.Separator();

        if ( ImGui.Button("Clear Conversation", new Vector2(-1, 0)) )
        {
            _showConfirmClearPopup = true;
        }

        // Optional: Add other controls like Apply Cleanup Strategy
        if ( ImGui.Button("Apply Cleanup", new Vector2(-1, 0)) )
        {
            _currentContext?.ApplyCleanupStrategy();
        }
    }

    private void RenderChatHistory(IConversationContext context, float availableWidth)
    {
        var maxBubbleWidth = availableWidth * 0.75f;
        var style          = ImGui.GetStyle();

        ImGui.Text("Conversation: Main");
        ImGui.Separator();

        ImGui.BeginChild("ChatScrollRegion", new Vector2(availableWidth, -1), ImGuiChildFlags.None);
        {
            for ( var turnIndex = 0; turnIndex < _historySnapshot.Count; turnIndex++ )
            {
                var turn = _historySnapshot[turnIndex];
                RenderTurn(context, turn, turnIndex, maxBubbleWidth, availableWidth);

                ImGui.Separator();
                ImGui.Spacing();
            }

            // if ( _pendingTurnSnapshot != null )
            // {
            //     ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.6f, 0.6f, 0.6f, 1.0f));
            //     RenderTurn(context, _pendingTurnSnapshot, -1, maxBubbleWidth, availableWidth, true);
            //     ImGui.PopStyleColor();
            // }

            if ( ImGui.GetScrollY() >= ImGui.GetScrollMaxY() - 10 )
            {
                ImGui.SetScrollHereY(1.0f);
            }
        }

        ImGui.EndChild();
    }

    private void RenderTurn(IConversationContext context, InteractionTurn turn, int turnIndex, float maxBubbleWidth, float availableWidth, bool isPending = false)
    {
        var style = ImGui.GetStyle();

        ImGui.TextDisabled($"Turn {turnIndex + 1} ({turn.StartTime.ToLocalTime():g})");
        ImGui.Spacing();

        for ( var msgIndex = 0; msgIndex < turn.Messages.Count; msgIndex++ )
        {
            var message     = turn.Messages[msgIndex];
            var participant = _participantsSnapshot.GetValueOrDefault(message.ParticipantId);
            var role        = participant?.Role ?? message.Role;

            var isUserMessage = role == ChatMessageRole.User;

            var messageText = message.Text;
            if ( message.IsPartial && isPending )
            {
                messageText += " ...";
            }

            ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(8, 8));
            ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 8.0f);

            var textSize     = ImGui.CalcTextSize(messageText, false, maxBubbleWidth - style.WindowPadding.X * 2);
            var nameSize     = ImGui.CalcTextSize(message.ParticipantName, false, maxBubbleWidth - style.WindowPadding.X * 2);
            var bubbleWidth  = Math.Min(textSize.X + style.WindowPadding.X * 2, maxBubbleWidth);
            var bubbleHeight = nameSize.Y + textSize.Y + style.WindowPadding.Y * 2 + (isUserMessage ? ImGui.GetTextLineHeight() : 0);

            var startPosX = style.WindowPadding.X;
            if ( isUserMessage )
            {
                startPosX = availableWidth - bubbleWidth - style.WindowPadding.X - style.ScrollbarSize;
            }

            ImGui.SetCursorPosX(startPosX);

            // --- Styling ---
            Vector4 bgColor,
                    textColor;

            if ( isUserMessage )
            {
                bgColor   = new Vector4(0.15f, 0.48f, 0.88f, 1.0f); // Blueish
                textColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f);    // White text
            }
            else
            {
                bgColor   = new Vector4(0.92f, 0.92f, 0.92f, 1.0f); // Light gray
                textColor = new Vector4(0.1f, 0.1f, 0.1f, 1.0f);    // Dark text
            }

            ImGui.PushStyleColor(ImGuiCol.ChildBg, bgColor);
            ImGui.PushStyleColor(ImGuiCol.Text, textColor);

            // --- Render Bubble ---
            var childId = $"msg_{turn.TurnId}_{message.MessageId}";
            ImGui.BeginChild(childId, new Vector2(bubbleWidth, bubbleHeight), ImGuiChildFlags.Borders, ImGuiWindowFlags.None);
            {
                if ( isUserMessage )
                {
                    ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.52f, 0.92f, 0.89f, 1f));
                    ImGui.TextWrapped(message.ParticipantName);
                    ImGui.PopStyleColor();
                }

                ImGui.TextWrapped(messageText);

                ImGui.EndChild();
            }

            ImGui.PopStyleColor(2);
            ImGui.PopStyleVar(2);

            // --- Context Menu (Edit/Delete) - Only for committed messages ---
            if ( !isPending && ImGui.BeginPopupContextItem($"ctx_{childId}") )
            {
                if ( ImGui.MenuItem("Edit") )
                {
                    _editTurnId         = turn.TurnId;
                    _editMessageId      = message.MessageId;
                    _editMessageContent = message.Text;
                    _showEditPopup      = true;
                }

                ImGui.Separator();
                if ( ImGui.MenuItem("Delete") )
                {
                    context.TryDeleteMessage(turn.TurnId, message.MessageId);
                }

                ImGui.EndPopup();
            }

            ImGui.Dummy(new Vector2(0, style.ItemSpacing.Y));
        }
    }

    private void RenderEditPopup(IConversationContext context)
    {
        if ( _showEditPopup )
        {
            ImGui.OpenPopup("Edit Message");
            _showEditPopup = false;
        }

        var viewport = ImGui.GetMainViewport();
        ImGui.SetNextWindowPos(viewport.Pos + viewport.Size * 0.5f, ImGuiCond.Appearing, new Vector2(0.5f, 0.5f));
        ImGui.SetNextWindowSize(new Vector2(500, 0), ImGuiCond.Appearing);

        if ( ImGui.BeginPopupModal("Edit Message", ImGuiWindowFlags.NoResize) )
        {
            ImGui.InputTextMultiline("##edit_content", ref _editMessageContent, 1024 * 4, new Vector2(0, 150));

            ImGui.Spacing();
            ImGui.Separator();
            ImGui.Spacing();

            var buttonWidth       = 120f;
            var totalButtonsWidth = buttonWidth * 2 + ImGui.GetStyle().ItemSpacing.X;
            ImGui.SetCursorPosX((ImGui.GetWindowWidth() - totalButtonsWidth) * 0.5f);

            if ( ImGui.Button("Save", new Vector2(buttonWidth, 0)) )
            {
                if ( _editMessageId != Guid.Empty && _editTurnId != Guid.Empty )
                {
                    context.TryUpdateMessage(_editTurnId, _editMessageId, _editMessageContent);

                    _editMessageId      = Guid.Empty;
                    _editTurnId         = Guid.Empty;
                    _editMessageContent = string.Empty;
                }

                ImGui.CloseCurrentPopup();
            }

            ImGui.SameLine();

            if ( ImGui.Button("Cancel", new Vector2(buttonWidth, 0)) )
            {
                _editMessageId      = Guid.Empty;
                _editTurnId         = Guid.Empty;
                _editMessageContent = string.Empty;
                ImGui.CloseCurrentPopup();
            }

            ImGui.EndPopup();
        }
        else
        {
            if ( _editMessageId == Guid.Empty )
            {
                return;
            }

            _editMessageId      = Guid.Empty;
            _editTurnId         = Guid.Empty;
            _editMessageContent = string.Empty;
        }
    }

    private void RenderConfirmClearPopup(IConversationContext context)
    {
        if ( _showConfirmClearPopup )
        {
            ImGui.OpenPopup("Confirm Clear");
            _showConfirmClearPopup = false;
        }

        var viewport = ImGui.GetMainViewport();
        ImGui.SetNextWindowPos(viewport.Pos + viewport.Size * 0.5f, ImGuiCond.Appearing, new Vector2(0.5f, 0.5f));

        if ( ImGui.BeginPopupModal("Confirm Clear", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize) )
        {
            ImGui.TextWrapped("Are you sure you want to clear the entire conversation history?");
            ImGui.TextWrapped("This action cannot be undone.");

            ImGui.Spacing();
            ImGui.Separator();
            ImGui.Spacing();

            var buttonWidth       = 120f;
            var totalButtonsWidth = buttonWidth * 2 + ImGui.GetStyle().ItemSpacing.X;
            ImGui.SetCursorPosX((ImGui.GetWindowWidth() - totalButtonsWidth) * 0.5f);

            if ( ImGui.Button("Yes, Clear", new Vector2(buttonWidth, 0)) )
            {
                _currentContext?.ClearHistory();
                ImGui.CloseCurrentPopup();
            }

            ImGui.SameLine();

            if ( ImGui.Button("No, Cancel", new Vector2(buttonWidth, 0)) )
            {
                ImGui.CloseCurrentPopup();
            }

            ImGui.EndPopup();
        }
    }
}

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Live2D.App;
using PersonaEngine.Lib.Live2D.Framework.Motion;

namespace PersonaEngine.Lib.Live2D.Behaviour;

/// <summary>
///     Manages Live2D model idle animations and custom automatic blinking.
///     Ensures a random idle animation from the 'Idle' group is playing if available
///     and handles periodic blinking by directly manipulating eye parameters.
///     Blinking operates independently of the main animation system.
/// </summary>
public sealed class IdleBlinkingAnimationService : ILive2DAnimationService
{
    private const string IDLE_MOTION_GROUP = "Idle";

    private const MotionPriority IDLE_MOTION_PRIORITY = MotionPriority.PriorityIdle;

    private const string PARAM_EYE_L_OPEN = "ParamEyeLOpen";

    private const string PARAM_EYE_R_OPEN = "ParamEyeROpen";

    private const float BLINK_INTERVAL_MIN_SECONDS = 1.5f;

    private const float BLINK_INTERVAL_MAX_SECONDS = 6.0f;

    private const float BLINK_CLOSING_DURATION = 0.06f;

    private const float BLINK_CLOSED_DURATION = 0.05f;

    private const float BLINK_OPENING_DURATION = 0.10f;

    private const float EYE_OPEN_VALUE = 1.0f;

    private const float EYE_CLOSED_VALUE = 0.0f;

    private readonly ILogger<IdleBlinkingAnimationService> _logger;

    private readonly Random _random = new();

    private float _blinkPhaseTimer = 0.0f;

    private BlinkState _currentBlinkState = BlinkState.Idle;

    private CubismMotionQueueEntry? _currentIdleMotionEntry;

    private bool _disposed = false;

    private bool _eyeParamsValid = false;

    private bool _isBlinking = false;

    private bool _isIdleAnimationAvailable = false;

    private bool _isStarted = false;

    private LAppModel? _model;

    private float _timeUntilNextBlink = 0.0f;

    /// <summary>
    ///     Initializes a new instance of the <see cref="IdleBlinkingAnimationService" /> class.
    /// </summary>
    /// <param name="logger">The logger instance for logging messages.</param>
    public IdleBlinkingAnimationService(ILogger<IdleBlinkingAnimationService> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        SetNextBlinkInterval();
    }

    #region Asset Validation

    /// <summary>
    ///     Checks if the required assets (idle motion group, eye parameters) are available in the model.
    ///     Sets the internal flags `_isIdleAnimationAvailable` and `_eyeParamsValid`.
    /// </summary>
    private void ValidateModelAssets()
    {
        if ( _model == null )
        {
            return;
        }

        _logger.LogDebug("Validating configured idle/blinking mappings against model assets...");

        _isIdleAnimationAvailable = false;
        foreach ( var motionKey in _model.Motions )
        {
            var groupName = LAppModel.GetMotionGroupName(motionKey);
            if ( groupName == IDLE_MOTION_GROUP )
            {
                _isIdleAnimationAvailable = true;
            }
        }

        if ( _isIdleAnimationAvailable )
        {
            _logger.LogDebug("Idle motion group '{IdleGroup}' is configured and available in the model.", IDLE_MOTION_GROUP);
        }
        else
        {
            _logger.LogWarning("Configured IDLE_MOTION_GROUP ('{IdleGroup}') not found in model! Idle animations disabled.", IDLE_MOTION_GROUP);
        }

        _eyeParamsValid = false;

        try
        {
            // An invalid index (usually -1) means the parameter doesn't exist.
            var leftEyeIndex  = _model.Model.GetParameterIndex(PARAM_EYE_L_OPEN);
            var rightEyeIndex = _model.Model.GetParameterIndex(PARAM_EYE_R_OPEN);

            if ( leftEyeIndex >= 0 && rightEyeIndex >= 0 )
            {
                _eyeParamsValid = true;
                _logger.LogDebug("Required eye parameters found: '{ParamL}' (Index: {IndexL}), '{ParamR}' (Index: {IndexR}).",
                                 PARAM_EYE_L_OPEN, leftEyeIndex, PARAM_EYE_R_OPEN, rightEyeIndex);
            }
            else
            {
                if ( leftEyeIndex < 0 )
                {
                    _logger.LogWarning("Eye parameter '{ParamL}' not found in the model.", PARAM_EYE_L_OPEN);
                }

                if ( rightEyeIndex < 0 )
                {
                    _logger.LogWarning("Eye parameter '{ParamR}' not found in the model.", PARAM_EYE_R_OPEN);
                }

                _logger.LogWarning("Automatic blinking disabled because one or both eye parameters are missing.");
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while validating eye parameters. Blinking disabled.");
            _eyeParamsValid = false;
        }
    }

    #endregion

    private enum BlinkState
    {
        Idle,

        Closing,

        Closed,

        Opening
    }

    #region ILive2DAnimationService Implementation

    /// <summary>
    ///     Starts the service with the specified Live2D model.
    ///     Validates required assets (idle motion group, eye parameters) and initializes state.
    /// </summary>
    /// <param name="model">The Live2D model instance to animate.</param>
    /// <exception cref="ArgumentNullException">Thrown if model is null.</exception>
    /// <exception cref="ObjectDisposedException">Thrown if the service has been disposed.</exception>
    public void Start(LAppModel model)
    {
        ObjectDisposedException.ThrowIf(_disposed, typeof(IdleBlinkingAnimationService));

        if ( _isStarted )
        {
            _logger.LogWarning("Service already started. Call Stop() first if reconfiguration is needed.");

            return;
        }

        _model = model ?? throw new ArgumentNullException(nameof(model));

        ValidateModelAssets();

        SetNextBlinkInterval();
        _currentBlinkState = BlinkState.Idle;
        _isBlinking        = false;
        _blinkPhaseTimer   = 0f;
        ResetEyesToOpenState();

        _currentIdleMotionEntry = null;

        _isStarted = true;
        _logger.LogInformation("IdleBlinkingAnimationService started. Idle animations: {IdleStatus}, Blinking: {BlinkStatus}.",
                               _isIdleAnimationAvailable ? "Enabled" : "Disabled",
                               _eyeParamsValid ? "Enabled" : "Disabled");

        if ( _isIdleAnimationAvailable && _currentIdleMotionEntry == null )
        {
            TryStartIdleMotion();
        }
    }

    /// <summary>
    ///     Stops the service, resetting blinking and clearing the tracked idle motion.
    ///     Attempts to leave the eyes in an open state.
    /// </summary>
    public void Stop()
    {
        if ( !_isStarted || _disposed )
        {
            return;
        }

        _isStarted = false;

        ResetEyesToOpenState();

        _isBlinking        = false;
        _currentBlinkState = BlinkState.Idle;
        _blinkPhaseTimer   = 0f;

        _currentIdleMotionEntry = null;
        // Note: The motion itself isn't forcibly stopped here; Instead we
        // rely on the model's lifecycle management to handle it.

        _logger.LogInformation("IdleBlinkingAnimationService stopped.");
    }

    /// <summary>
    ///     Updates the idle animation and blinking state based on elapsed time.
    ///     Should be called once per frame.
    /// </summary>
    /// <param name="deltaTime">The time elapsed since the last update call, in seconds.</param>
    public void Update(float deltaTime)
    {
        if ( !_isStarted || _model?.Model == null || _disposed || deltaTime <= 0.0f )
        {
            return;
        }

        UpdateIdleMotion();

        if ( _eyeParamsValid )
        {
            UpdateBlinking(deltaTime);
        }
    }

    #endregion

    #region Core Logic: Idle

    private void UpdateIdleMotion()
    {
        if ( _currentIdleMotionEntry is { Finished: true } )
        {
            _logger.LogTrace("Tracked idle motion finished.");
            _currentIdleMotionEntry = null;
        }

        if ( _isIdleAnimationAvailable && _currentIdleMotionEntry == null )
        {
            TryStartIdleMotion();
        }
    }

    private void TryStartIdleMotion()
    {
        if ( _model == null )
        {
            return;
        }

        _logger.LogTrace("Attempting to start a new idle motion for group '{IdleGroup}'.", IDLE_MOTION_GROUP);

        try
        {
            var newEntry = _model.StartRandomMotion(IDLE_MOTION_GROUP, IDLE_MOTION_PRIORITY);

            _currentIdleMotionEntry = newEntry;

            if ( _currentIdleMotionEntry != null )
            {
                _logger.LogDebug("Successfully started idle motion.");
            }
            // _logger.LogWarning("Failed to start idle motion for group '{IdleGroup}'. The group might be empty or invalid.", IDLE_MOTION_GROUP);
            // Optionally disable idle animations if it fails consistently?
            // We don't because sometimes this occurs when another animation with the same or higher priority is also playing.
            // _isIdleAnimationAvailable = false;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Exception occurred while trying to start idle motion for group '{IdleGroup}'. Disabling idle animations.", IDLE_MOTION_GROUP);
            _isIdleAnimationAvailable = false;
            _currentIdleMotionEntry   = null;
        }
    }

    #endregion

    #region Core Logic: Blinking

    private void UpdateBlinking(float deltaTime)
    {
        try
        {
            if ( _isBlinking )
            {
                _blinkPhaseTimer += deltaTime;
                float eyeValue;

                switch ( _currentBlinkState )
                {
                    case BlinkState.Closing:
                        // Calculate progress towards closed state (1.0 -> 0.0)
                        eyeValue = Math.Max(EYE_CLOSED_VALUE, EYE_OPEN_VALUE - _blinkPhaseTimer / BLINK_CLOSING_DURATION);
                        if ( _blinkPhaseTimer >= BLINK_CLOSING_DURATION )
                        {
                            _currentBlinkState = BlinkState.Closed;
                            _blinkPhaseTimer   = 0f;
                            eyeValue           = EYE_CLOSED_VALUE;
                            _logger.LogTrace("Blink phase: Closed");
                        }

                        break;

                    case BlinkState.Closed:
                        eyeValue = EYE_CLOSED_VALUE;
                        if ( _blinkPhaseTimer >= BLINK_CLOSED_DURATION )
                        {
                            _currentBlinkState = BlinkState.Opening;
                            _blinkPhaseTimer   = 0f;
                            _logger.LogTrace("Blink phase: Opening");
                        }

                        break;

                    case BlinkState.Opening:
                        // Calculate progress towards open state (0.0 -> 1.0)
                        eyeValue = Math.Min(EYE_OPEN_VALUE, EYE_CLOSED_VALUE + _blinkPhaseTimer / BLINK_OPENING_DURATION);
                        if ( _blinkPhaseTimer >= BLINK_OPENING_DURATION )
                        {
                            _isBlinking        = false;
                            _currentBlinkState = BlinkState.Idle;
                            SetNextBlinkInterval();
                            eyeValue = EYE_OPEN_VALUE;
                            _logger.LogTrace("Blink finished. Next blink in {Interval:F2}s", _timeUntilNextBlink);
                            SetEyeParameters(eyeValue);

                            return; // Exit early as state is reset
                        }

                        break;

                    case BlinkState.Idle:
                    default:
                        _logger.LogWarning("Invalid blink state detected while _isBlinking was true. Resetting blink state.");
                        _isBlinking        = false;
                        _currentBlinkState = BlinkState.Idle;
                        SetNextBlinkInterval();
                        ResetEyesToOpenState();

                        return; // Exit early
                }

                SetEyeParameters(eyeValue);
            }
            else
            {
                _timeUntilNextBlink -= deltaTime;
                if ( _timeUntilNextBlink <= 0f )
                {
                    // Start a new blink cycle
                    _isBlinking        = true;
                    _currentBlinkState = BlinkState.Closing;
                    _blinkPhaseTimer   = 0f;
                    _logger.LogTrace("Starting blink.");
                    // Set initial closing value immediately? Or wait for next frame?
                    // Current logic waits for the next frame's update. This is usually fine.
                }
                // IMPORTANT: Do not call SetEyeParameters here when idle.
                // Other animations (like facial expressions) might be controlling the eyes.
                // We only take control during the blink itself.
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during blinking update. Disabling blinking for safety.");
            _eyeParamsValid    = false; // Prevent further blinking attempts
            _isBlinking        = false;
            _currentBlinkState = BlinkState.Idle;
            ResetEyesToOpenState();
        }
    }

    /// <summary>
    ///     Sets the values for the left and right eye openness parameters.
    ///     Includes error handling to disable blinking if parameters become invalid.
    /// </summary>
    /// <param name="value">The value to set (typically between 0.0 and 1.0).</param>
    private void SetEyeParameters(float value)
    {
        // Redundant checks, but safe:
        if ( _model?.Model == null || !_eyeParamsValid )
        {
            return;
        }

        try
        {
            _model.Model.SetParameterValue(PARAM_EYE_L_OPEN, value);
            _model.Model.SetParameterValue(PARAM_EYE_R_OPEN, value);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to set eye parameters (L:'{ParamL}', R:'{ParamR}') to value {Value}. Disabling blinking.",
                             PARAM_EYE_L_OPEN, PARAM_EYE_R_OPEN, value);

            _eyeParamsValid    = false;
            _isBlinking        = false;
            _currentBlinkState = BlinkState.Idle;
            // Don't try to reset eyes here, as the SetParameterValue call itself failed.
        }
    }

    private void ResetEyesToOpenState()
    {
        if ( _model?.Model != null && _eyeParamsValid )
        {
            _logger.LogTrace("Attempting to reset eyes to open state.");
            SetEyeParameters(EYE_OPEN_VALUE);
        }
        else
        {
            _logger.LogTrace("Skipping reset eyes to open state (Model null or eye params invalid).");
        }
    }

    /// <summary>
    ///     Calculates and sets the random time interval until the next blink should start.
    /// </summary>
    private void SetNextBlinkInterval()
    {
        _timeUntilNextBlink = (float)(_random.NextDouble() *
                                      (BLINK_INTERVAL_MAX_SECONDS - BLINK_INTERVAL_MIN_SECONDS) +
                                      BLINK_INTERVAL_MIN_SECONDS);
    }

    #endregion

    #region IDisposable Implementation

    public void Dispose() { Dispose(true); }

    private void Dispose(bool disposing)
    {
        if ( !_disposed )
        {
            if ( disposing )
            {
                _logger.LogDebug("Disposing IdleBlinkingAnimationService...");
                Stop();
                _model = null;
                _logger.LogInformation("IdleBlinkingAnimationService disposed.");
            }

            _disposed = true;
        }
    }

    #endregion
}

using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.Live2D.App;
using PersonaEngine.Lib.Live2D.Behaviour;
using PersonaEngine.Lib.Live2D.Framework.Rendering;
using PersonaEngine.Lib.UI;

using Silk.NET.Input;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

namespace PersonaEngine.Lib.Live2D;

public class Live2DManager : IRenderComponent
{
    private readonly IList<ILive2DAnimationService> _live2DAnimationServices;

    private readonly IOptionsMonitor<Live2DOptions> _options;

    private LAppDelegate? _lapp;

    public Live2DManager(IOptionsMonitor<Live2DOptions> options, IEnumerable<ILive2DAnimationService> live2DAnimationServices)
    {
        _options                 = options;
        _live2DAnimationServices = live2DAnimationServices.ToList();
    }

    public bool UseSpout => true;

    public string SpoutTarget => "Live2D";

    public int Priority => 100;

    public void Update(float deltaTime) { }

    public void Render(float deltaTime)
    {
        if ( _lapp == null )
        {
            return;
        }

        _lapp.Update(deltaTime);
        _lapp.Run();
    }

    public void Resize() { _lapp?.Resize(); }

    public void Initialize(GL gl, IView view, IInputContext input)
    {
        var config = _options.CurrentValue;
        _lapp = new LAppDelegate(new SilkNetApi(gl, config.Width, config.Height), _ => { }) { BGColor = new CubismTextureColor(0, 0, 0, 0) };

        LoadModel(config.ModelPath, config.ModelName);
    }

    public void Dispose()
    {
        // Context is destroyed anyway when app closes.
    }

    /// <summary>
    ///     Loads a Live2D model from the given path.
    /// </summary>
    public void LoadModel(string modelPath, string modelName)
    {
        if ( _lapp == null )
        {
            throw new InvalidOperationException("Live2DManager is not initialized.");
        }

        var model = _lapp.Live2dManager.LoadModel(modelPath, modelName);
        model.RandomMotion      = false;
        model.CustomValueUpdate = true;

        // model.ModelMatrix.Translate(0.0f, -1.8f);
        model.ModelMatrix.ScaleRelative(0.7f, 0.7f);

        Resize();

        foreach ( var animationService in _live2DAnimationServices )
        {
            model.ValueUpdate += _ => animationService.Update(LAppPal.DeltaTime);
            animationService.Start(model);
        }
    }
}

using System.Text;
using System.Text.RegularExpressions;

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.LLM;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
///     Processor for extracting emotion tags from text before TTS synthesis
/// </summary>
public class EmotionProcessor(IEmotionService emotionService, ILoggerFactory loggerFactory) : ITextFilter
{
    private const string MetadataKey = "EmotionMarkers";

    private const string MarkerPrefix = "__EM";

    private const string MarkerSuffix = "__";

    private static readonly Regex EmotionTagRegex = new(@"\[EMOTION:(.*?)\]",
                                                        RegexOptions.Compiled | RegexOptions.CultureInvariant);

    private readonly ILogger<EmotionProcessor> _logger = loggerFactory.CreateLogger<EmotionProcessor>();

    public int Priority => 100;

    public ValueTask<TextFilterResult> ProcessAsync(string text, CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return ValueTask.FromResult(new TextFilterResult { ProcessedText = text });
        }

        cancellationToken.ThrowIfCancellationRequested();

        var markers              = new List<EmotionMarkerInfo>();
        var processedTextBuilder = new StringBuilder();
        var lastIndex            = 0;
        var markerIndex          = 0;

        var matches = EmotionTagRegex.Matches(text);
        if ( matches.Count == 0 )
        {
            _logger.LogTrace("No emotion tags found in the input text.");

            return ValueTask.FromResult(new TextFilterResult { ProcessedText = text });
        }

        _logger.LogDebug("Found {Count} potential emotion tags.", matches.Count);

        foreach ( Match match in matches )
        {
            cancellationToken.ThrowIfCancellationRequested();

            processedTextBuilder.Append(text, lastIndex, match.Index - lastIndex);
            var emotionValue = match.Groups[1].Value;
            if ( string.IsNullOrWhiteSpace(emotionValue) )
            {
                _logger.LogWarning("Found emotion tag with empty value at index {Index}. Skipping.", match.Index);
                processedTextBuilder.Append(match.Value);
                lastIndex = match.Index + match.Length;

                continue;
            }

            // e.g., [__EM0__](//), [__EM1__](//) This causes the phonemizer to see it as a feature and ignore it
            var markerId    = $"{MarkerPrefix}{markerIndex++}{MarkerSuffix}";
            var markerToken = $"[{markerId}](//)";
            processedTextBuilder.Append(markerToken);

            var markerInfo = new EmotionMarkerInfo { MarkerId = markerId, Emotion = emotionValue };
            markers.Add(markerInfo);

            _logger.LogTrace("Replaced tag '{Tag}' with marker '{Marker}' for emotion '{Emotion}'.",
                             match.Value, markerId, emotionValue);

            lastIndex = match.Index + match.Length;
        }

        processedTextBuilder.Append(text, lastIndex, text.Length - lastIndex);
        var processedText = processedTextBuilder.ToString();
        _logger.LogDebug("Processed text length: {Length}. Original length: {OriginalLength}.",
                         processedText.Length, text.Length);

        var metadata = new Dictionary<string, object>();
        if ( markers.Count != 0 )
        {
            metadata[MetadataKey] = markers;
        }

        var result = new TextFilterResult { ProcessedText = processedText, Metadata = metadata };

        return ValueTask.FromResult(result);
    }

    public ValueTask PostProcessAsync(TextFilterResult textFilterResult, AudioSegment segment, CancellationToken cancellationToken = default)
    {
        if ( !segment.Tokens.Any() ||
             !textFilterResult.Metadata.TryGetValue(MetadataKey, out var markersObj) ||
             markersObj is not List<EmotionMarkerInfo> markers || markers.Count == 0 )
        {
            _logger.LogTrace("No emotion markers found in metadata or segment/tokens are missing/empty for segment Id {SegmentId}. Skipping post-processing.", segment?.Id);

            return ValueTask.CompletedTask;
        }

        _logger.LogDebug("Starting emotion post-processing for segment Id {SegmentId} with {MarkerCount} markers.", segment.Id, markers.Count);

        cancellationToken.ThrowIfCancellationRequested();

        var emotionTimings   = new List<EmotionTiming>();
        var processedMarkers = new HashSet<string>();
        var allMarkersFound  = false;

        var markerDict = markers.ToDictionary(m => m.MarkerId, m => m.Emotion);

        foreach ( var token in segment.Tokens )
        {
            cancellationToken.ThrowIfCancellationRequested();

            foreach ( var (markerId, emotion) in markerDict )
            {
                if ( processedMarkers.Contains(markerId) || !token.Text.Contains(markerId) )
                {
                    continue;
                }

                // Usually okay, the only case wher StartTS is 0 is if it's the first token.
                var timestamp = token.StartTs ?? 0;

                emotionTimings.Add(new EmotionTiming { Timestamp = timestamp, Emotion = emotion });
                processedMarkers.Add(markerId);
                token.Text = string.Empty;

                _logger.LogDebug("Mapped emotion '{Emotion}' (from marker '{Marker}') to timestamp {Timestamp:F2}s based on token '{TokenText}'.",
                                 emotion, markerId, timestamp, token.Text);

                if ( processedMarkers.Count == markers.Count )
                {
                    _logger.LogDebug("All {Count} markers have been processed. Exiting token scan early.", markers.Count);
                    allMarkersFound = true;

                    break;
                }
            }

            if ( allMarkersFound )
            {
                break;
            }
        }

        if ( emotionTimings.Count != 0 )
        {
            _logger.LogInformation("Registering {Count} timed emotions for segment Id {SegmentId}.", emotionTimings.Count, segment.Id);
            try
            {
                emotionService.RegisterEmotions(segment.Id, emotionTimings);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error registering emotions for segment Id {SegmentId}.", segment.Id);
            }
        }
        else
        {
            _logger.LogWarning("Could not map any emotion markers to timestamps for segment Id {SegmentId}. This might happen if markers were removed by later filters or TTS engine issues.", segment.Id);
        }

        if ( processedMarkers.Count >= markers.Count )
        {
            return ValueTask.CompletedTask;
        }

        {
            var unprocessed = markers.Where(m => !processedMarkers.Contains(m.MarkerId)).Select(m => m.MarkerId);
            _logger.LogWarning("The following emotion markers were found in ProcessAsync but not located in the final tokens for segment Id {SegmentId}: {UnprocessedMarkers}",
                               segment.Id, string.Join(", ", unprocessed));
        }

        return ValueTask.CompletedTask;
    }
}

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Microsoft.ML.Tokenizers;

namespace PersonaEngine.Lib.TTS.Profanity;

public class ProfanityDetectorOnnx : IDisposable
{
    private readonly InferenceSession _session;

    private readonly Tokenizer _tokenizer;

    public ProfanityDetectorOnnx(string? modelPath = null, string? vocabPath = null)
    {
        if ( modelPath == null )
        {
            modelPath = ModelUtils.GetModelPath(ModelType.TinyToxic);
        }

        if ( vocabPath == null )
        {
            vocabPath = ModelUtils.GetModelPath(ModelType.TinyToxicVocab);
        }

        var options = new SessionOptions {
                                             EnableMemoryPattern    = true,
                                             ExecutionMode          = ExecutionMode.ORT_PARALLEL,
                                             InterOpNumThreads      = Environment.ProcessorCount,
                                             IntraOpNumThreads      = Environment.ProcessorCount,
                                             GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
                                             LogSeverityLevel       = OrtLoggingLevel.ORT_LOGGING_LEVEL_FATAL
                                         };

        options.AppendExecutionProvider_CPU();

        _session = new InferenceSession(modelPath, options);

        using Stream vocabStream = File.OpenRead(vocabPath);
        _tokenizer = BertTokenizer.Create(vocabStream);
    }

    public void Dispose() { _session?.Dispose(); }

    private IEnumerable<long> Tokenize(string sentence) { return _tokenizer.EncodeToIds(sentence).Select(x => (long)x); }

    public float Run(string sentence)
    {
        var inputIds       = Tokenize(sentence).ToArray();
        var inputIdsTensor = new DenseTensor<long>(inputIds, new[] { 1, inputIds.Length });

        var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input_ids", inputIdsTensor) };

        using var results = _session.Run(inputs);

        return results[0].AsTensor<float>().ToArray().Single();
    }
}

using System.Threading.Channels;

using Microsoft.Extensions.Logging;

using OpenAI.Chat;

using PersonaEngine.Lib.ASR.Transcriber;
using PersonaEngine.Lib.Audio;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Input;

using Polly;
using Polly.Contrib.WaitAndRetry;
using Polly.Retry;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Adapters.Audio.Input;

public sealed class MicrophoneInputAdapter(
    IMicrophone                     microphone,
    IRealtimeSpeechTranscriptor     speechTranscriptor,
    ILogger<MicrophoneInputAdapter> logger)
    : IAudioInputAdapter
{
    private readonly AsyncRetryPolicy _transcriptionRetyPolicy = Policy
                                                                 .Handle<RestartTranscriptionException>()
                                                                 .Or<Exception>(ex => ex is not OperationCanceledException)
                                                                 .WaitAndRetryAsync(Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(1000), 3),
                                                                                async (exception, timespan, retryAttempt, context) =>
                                                                                {
                                                                                var sessionId = context.TryGetValue("sessionId", out var value) ? (Guid)value : Guid.Empty;
                                                                                logger.LogWarning("Transcription failed/stopped for session {SessionId}. Retrying in {Timespan}. Attempt {RetryAttempt}...",
                                                                                sessionId, timespan, retryAttempt);

                                                                                await Task.CompletedTask;
                                                                                });

    private ChannelWriter<IInputEvent>? _inputWriter;

    private bool _isDisposed;

    private Guid? _sessionId;

    private CancellationTokenSource? _transcriptionCts;

    private Task? _transcriptionTask;

    public Guid AdapterId { get; } = Guid.NewGuid();

    public ParticipantInfo Participant { get; } = new(Guid.NewGuid().ToString(), "User", ChatMessageRole.User);

    public IAwaitableAudioSource GetAudioSource() { return microphone; }

    public ValueTask InitializeAsync(Guid sessionId, ChannelWriter<IInputEvent> inputWriter, CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_isDisposed, this);

        if ( _transcriptionTask is not null && !_transcriptionTask.IsCompleted )
        {
            logger.LogWarning("Adapter is already running. Initialization skipped.");

            return ValueTask.CompletedTask;
        }

        _sessionId   = sessionId;
        _inputWriter = inputWriter ?? throw new ArgumentNullException(nameof(inputWriter));

        return ValueTask.CompletedTask;
    }

    public ValueTask StartAsync(CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_isDisposed, this);

        if ( _inputWriter is null || _sessionId is null )
        {
            throw new InvalidOperationException("Adapter must be initialized before starting.");
        }

        if ( _transcriptionTask is not null && !_transcriptionTask.IsCompleted )
        {
            logger.LogInformation("Transcription task is already running.");

            return ValueTask.CompletedTask;
        }

        _transcriptionCts?.Dispose();
        _transcriptionCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        var localCt        = _transcriptionCts.Token;
        var localWriter    = _inputWriter;
        var localSessionId = _sessionId.Value;

        microphone.StartRecording();

        _transcriptionTask = Task.Run(async () => await TranscriptionJobWithRetry(localSessionId, localWriter, localCt), localCt);

        return ValueTask.CompletedTask;
    }

    public async ValueTask StopAsync(CancellationToken cancellationToken)
    {
        if ( _isDisposed )
        {
            return;
        }

        logger.LogDebug("Stopping microphone recording and transcription task.");
        microphone.StopRecording();

        if ( _transcriptionCts is { IsCancellationRequested: false } )
        {
            await _transcriptionCts.CancelAsync();
        }

        if ( _transcriptionTask != null )
        {
            try
            {
                await _transcriptionTask.WaitAsync(cancellationToken);
            }
            catch (OperationCanceledException)
            {
                logger.LogInformation("Transcription task cancellation confirmed during stop.");
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Exception waiting for transcription task to complete during stop: {Message}", ex.Message);
            }
            finally
            {
                _transcriptionTask = null;
            }
        }

        _transcriptionCts?.Dispose();
        _transcriptionCts = null;
        _inputWriter      = null;
        _sessionId        = null;

        logger.LogDebug("Adapter stopped.");
    }

    public async ValueTask DisposeAsync()
    {
        if ( _isDisposed )
        {
            return;
        }

        await StopAsync(CancellationToken.None);

        _transcriptionCts?.Dispose();

        _isDisposed = true;
    }

    private async ValueTask TranscriptionJobWithRetry(Guid sessionId, ChannelWriter<IInputEvent> writer, CancellationToken ct)
    {
        logger.LogDebug("Transcription job starting for session {SessionId}.", sessionId);
        var pollyContext = new Polly.Context($"TranscriptionJob_{sessionId}") { ["sessionId"] = sessionId };

        try
        {
            await _transcriptionRetyPolicy.ExecuteAsync(
                                                        async (_, token) => await RunTranscriptionLoop(sessionId, writer, token),
                                                        pollyContext,
                                                        ct);
        }
        catch (OperationCanceledException)
        {
            logger.LogDebug("Transcription job cancelled for session {SessionId}.", sessionId);
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            logger.LogError(ex, "Unhandled error during transcription job for session {SessionId} after retries (if any): {Message}", sessionId, ex.Message);
        }
        finally
        {
            logger.LogInformation("Transcription job execution flow completed for session {SessionId}.", sessionId);
        }
    }

    private async ValueTask RunTranscriptionLoop(Guid sessionId, ChannelWriter<IInputEvent> writer, CancellationToken ct)
    {
        logger.LogDebug("Transcription job starting for session {SessionId}.", sessionId);

        var eventsLoop = speechTranscriptor.TranscribeAsync(microphone, ct);

        await foreach ( var transcriptionEvent in eventsLoop )
        {
            if ( transcriptionEvent is RealtimeSessionStopped )
            {
                throw new RestartTranscriptionException();
            }

            if ( transcriptionEvent is not IRealtimeTranscriptionSegment transcriptionEventSeg )
            {
                continue;
            }

            var inputEvent = MapToInputEvent(transcriptionEventSeg, sessionId);

            if ( inputEvent != null )
            {
                await writer.WriteAsync(inputEvent, ct);
            }
        }
    }

    private IInputEvent? MapToInputEvent(IRealtimeTranscriptionSegment transcriptionEvent, Guid sessionId)
    {
        return transcriptionEvent switch {
            RealtimeSegmentRecognized recognized => new SttSegmentRecognized(
                                                                             sessionId,
                                                                             DateTimeOffset.UtcNow,
                                                                             Participant.Id,
                                                                             recognized.Segment.Text,
                                                                             recognized.Segment.Duration,
                                                                             recognized.Segment.ConfidenceLevel),

            RealtimeSegmentRecognizing recognizing => new SttSegmentRecognizing(
                                                                                sessionId,
                                                                                DateTimeOffset.UtcNow,
                                                                                Participant.Id,
                                                                                recognizing.Segment.Text,
                                                                                recognizing.Segment.Duration),

            _ => null
        };
    }

    private class RestartTranscriptionException() : Exception("Transcription session stopped and needs restart.");
}

using System.Text;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Connectors.OpenAI;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.LLM;

namespace PersonaEngine.Lib.Vision;

public class VisualQAService : IVisualQAService
{
    private readonly WindowCaptureService _captureService;

    private readonly IVisualChatEngine _chatEngine;

    private readonly VisionConfig _config;

    private readonly SemaphoreSlim _fileChangeSemaphore = new(1, 1);

    private readonly CancellationTokenSource _internalCts = new();

    private readonly ILogger<VisualQAService> _logger;

    private DateTimeOffset _lastProcessedTimestamp = DateTimeOffset.MinValue;

    public VisualQAService(
        IOptions<AvatarAppConfig> config,
        WindowCaptureService      captureService,
        IVisualChatEngine         chatEngine,
        ILogger<VisualQAService>  logger)
    {
        _config = config.Value.Vision;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        _captureService = captureService;
        _chatEngine     = chatEngine;

        _captureService.OnCaptureFrame += OnCaptureFrame;
    }

    public string? ScreenCaption { get; private set; }

    public Task StartAsync(CancellationToken cancellationToken = default)
    {
        if ( !_config.Enabled )
        {
            return Task.CompletedTask;
        }

        CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _internalCts.Token);
        _logger.LogDebug("Starting Visual QA Service");
        _captureService.StartAsync(cancellationToken);

        return Task.CompletedTask;
    }

    public async Task StopAsync()
    {
        _logger.LogDebug("Stopping Visual QA Service");
        await _internalCts.CancelAsync();
        await _captureService.StopAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await StopAsync();
        _internalCts.Dispose();
        _fileChangeSemaphore.Dispose();
        _captureService.OnCaptureFrame -= OnCaptureFrame;
    }

    private async void OnCaptureFrame(object? sender, CaptureFrameEventArgs e)
    {
        await _fileChangeSemaphore.WaitAsync(_internalCts.Token);

        try
        {
            var currentTimestamp = DateTimeOffset.Now;
            if ( (currentTimestamp - _lastProcessedTimestamp).TotalMilliseconds < 100 )
            {
                return;
            }

            _lastProcessedTimestamp = currentTimestamp;

            var result = await ProcessVisualQAFrame(e.FrameData);
            ScreenCaption = result;
        }
        finally
        {
            _fileChangeSemaphore.Release();
        }
    }

    private async Task<string?> ProcessVisualQAFrame(ReadOnlyMemory<byte> imageData)
    {
        try
        {
            var question    = "Describe the main content of this screenshot objectivly in great detail.";
            var chatMessage = new VisualChatMessage(question, imageData);
            var caption     = new StringBuilder();
            var settings    = new OpenAIPromptExecutionSettings { FrequencyPenalty = 1.1, Temperature = 0.1, PresencePenalty = 1.1, MaxTokens = 256 };
            await foreach ( var response in _chatEngine.GetStreamingChatResponseAsync(chatMessage, settings, _internalCts.Token) )
            {
                caption.Append(response);
            }

            return caption.ToString();
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Failed to caption screen frame");

            return null;
        }
    }
}

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

using uniffi.rust_lib;

namespace PersonaEngine.Lib.Vision;

public class CaptureFrameEventArgs : EventArgs
{
    public CaptureFrameEventArgs(ReadOnlyMemory<byte> frameData) { FrameData = frameData; }

    public ReadOnlyMemory<byte> FrameData { get; }
}

public class WindowCaptureService : IDisposable
{
    private readonly VisionConfig _config;

    private readonly CancellationTokenSource _cts = new();

    private readonly Lock _lock = new();

    private readonly ILogger<WindowCaptureService> _logger;

    private Task? _captureTask;

    public WindowCaptureService(IOptions<AvatarAppConfig> config, ILogger<WindowCaptureService>? logger = null)

    {
        _config = config.Value.Vision ?? throw new ArgumentNullException(nameof(config));
        _logger = logger ?? NullLogger<WindowCaptureService>.Instance;
    }

    public void Dispose()
    {
        StopAsync().GetAwaiter().GetResult();
        _cts.Dispose();
        GC.SuppressFinalize(this);
    }

    public event EventHandler<CaptureFrameEventArgs>? OnCaptureFrame;

    private void HandleCaptureFrame(ReadOnlyMemory<byte> frameData)
    {
        try
        {
            OnCaptureFrame?.Invoke(this, new CaptureFrameEventArgs(frameData));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error raising PlaybackStarted event");
        }
    }

    public Task StartAsync(CancellationToken cancellationToken = default)
    {
        lock (_lock)
        {
            if ( _captureTask != null )
            {
                return Task.CompletedTask;
            }

            _captureTask = Task.Run(() => CaptureLoop(_cts.Token), cancellationToken);
        }

        return Task.CompletedTask;
    }

    public async Task StopAsync()
    {
        lock (_lock)
        {
            if ( _captureTask == null )
            {
                return;
            }

            _cts.Cancel();
        }

        try
        {
            await _captureTask.ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            // Expected when cancellation is requested.
        }
        finally
        {
            lock (_lock)
            {
                _captureTask = null;
            }
        }
    }

    private async Task CaptureLoop(CancellationToken token)
    {
        while ( !token.IsCancellationRequested )
        {
            try
            {
                if ( OnCaptureFrame == null )
                {
                    return;
                }

                var result = RustLibMethods.CaptureWindowByTitle(_config.WindowTitle);
                if ( result.image != null )
                {
                    var imageData = await ProcessImage(result, token);
                    HandleCaptureFrame(imageData);
                }
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error during capture");
            }

            try
            {
                await Task.Delay(_config.CaptureInterval, token).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                break;
            }
        }
    }

    private async Task<ReadOnlyMemory<byte>> ProcessImage(ImageData imageData, CancellationToken token)
    {
        var minPixels = _config.CaptureMinPixels;
        var maxPixels = _config.CaptureMaxPixels;

        var       width         = (int)imageData.width;
        var       height        = (int)imageData.height;
        using var image         = Image.LoadPixelData<Rgba32>(imageData.image, width, height);
        var       currentPixels = width * height;
        if ( currentPixels < minPixels || currentPixels > maxPixels )
        {
            double scaleFactor;
            if ( currentPixels < minPixels )
            {
                scaleFactor = Math.Sqrt((double)minPixels / currentPixels);
            }
            else
            {
                scaleFactor = Math.Sqrt((double)maxPixels / currentPixels);
            }

            var newWidth  = (int)(width * scaleFactor);
            var newHeight = (int)(height * scaleFactor);

            image.Mutate(x => x.Resize(newWidth, newHeight));
        }

        using var memStream = new MemoryStream();
        await image.SaveAsync(memStream, PngFormat.Instance, token);

        return new ReadOnlyMemory<byte>(memStream.ToArray());
    }
}

namespace PersonaEngine.Lib.Utils;

public class WavUtils
{
    private const int RIFF_CHUNK_ID = 0x46464952; // "RIFF" in ASCII

    private const int WAVE_FORMAT = 0x45564157; // "WAVE" in ASCII

    private const int FMT_CHUNK_ID = 0x20746D66; // "fmt " in ASCII

    private const int DATA_CHUNK_ID = 0x61746164; // "data" in ASCII

    private const int PCM_FORMAT = 1; // PCM audio format

    public static void SaveToWav(Memory<float> samples, string filePath, int sampleRate = 44100, int channels = 1)
    {
        using (var writer = new BinaryWriter(File.OpenWrite(filePath)))
        {
            WriteWavFile(writer, samples, sampleRate, channels);
        }
    }

    public static void AppendToWav(Memory<float> samples, string filePath)
    {
        // First, read the existing WAV file header
        WavHeader header;
        long      dataPosition;
        using (var reader = new BinaryReader(File.OpenRead(filePath)))
        {
            header       = ReadWavHeader(reader);
            dataPosition = reader.BaseStream.Position;
        }

        // Open file for writing and seek to end of data
        using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite))
        using (var writer = new BinaryWriter(stream))
        {
            stream.Seek(0, SeekOrigin.End);

            // Write new samples
            var span = samples.Span;
            for ( var i = 0; i < span.Length; i++ )
            {
                var pcm = (short)(span[i] * short.MaxValue);
                writer.Write(pcm);
            }

            // Update file size in header
            var newDataSize = header.DataSize + samples.Length * sizeof(short);
            var newFileSize = newDataSize + 44 - 8; // 44 is header size, subtract 8 for RIFF header

            stream.Seek(4, SeekOrigin.Begin);
            writer.Write(newFileSize);

            // Update data chunk size
            stream.Seek(dataPosition - 4, SeekOrigin.Begin);
            writer.Write(newDataSize);
        }
    }

    private static void WriteWavFile(BinaryWriter writer, Memory<float> samples, int sampleRate, int channels)
    {
        var bytesPerSample = sizeof(short); // We'll convert float to 16-bit PCM
        var dataSize       = samples.Length * bytesPerSample;
        var headerSize     = 44;                        // Standard WAV header size
        var fileSize       = headerSize + dataSize - 8; // Total file size - 8 bytes

        // Write WAV header
        writer.Write(RIFF_CHUNK_ID); // "RIFF" chunk
        writer.Write(fileSize);      // File size - 8
        writer.Write(WAVE_FORMAT);   // "WAVE" format

        // Write format chunk
        writer.Write(FMT_CHUNK_ID);                           // "fmt " chunk
        writer.Write(16);                                     // Format chunk size (16 for PCM)
        writer.Write((short)PCM_FORMAT);                      // Audio format (1 for PCM)
        writer.Write((short)channels);                        // Number of channels
        writer.Write(sampleRate);                             // Sample rate
        writer.Write(sampleRate * channels * bytesPerSample); // Byte rate
        writer.Write((short)(channels * bytesPerSample));     // Block align
        writer.Write((short)(bytesPerSample * 8));            // Bits per sample

        // Write data chunk
        writer.Write(DATA_CHUNK_ID); // "data" chunk
        writer.Write(dataSize);      // Data size

        // Write audio samples
        var span = samples.Span;
        for ( var i = 0; i < span.Length; i++ )
        {
            var pcm = (short)(span[i] * short.MaxValue);
            writer.Write(pcm);
        }
    }

    private static WavHeader ReadWavHeader(BinaryReader reader)
    {
        // Verify RIFF header
        if ( reader.ReadInt32() != RIFF_CHUNK_ID )
        {
            throw new InvalidDataException("Not a valid RIFF file");
        }

        var header = new WavHeader { FileSize = reader.ReadInt32() };

        // Verify WAVE format
        if ( reader.ReadInt32() != WAVE_FORMAT )
        {
            throw new InvalidDataException("Not a valid WAVE file");
        }

        // Read format chunk
        if ( reader.ReadInt32() != FMT_CHUNK_ID )
        {
            throw new InvalidDataException("Missing format chunk");
        }

        var fmtSize = reader.ReadInt32();
        if ( reader.ReadInt16() != PCM_FORMAT )
        {
            throw new InvalidDataException("Unsupported audio format (must be PCM)");
        }

        header.Channels      = reader.ReadInt16();
        header.SampleRate    = reader.ReadInt32();
        header.ByteRate      = reader.ReadInt32();
        header.BlockAlign    = reader.ReadInt16();
        header.BitsPerSample = reader.ReadInt16();

        // Skip any extra format bytes
        if ( fmtSize > 16 )
        {
            reader.BaseStream.Seek(fmtSize - 16, SeekOrigin.Current);
        }

        // Find data chunk
        while ( true )
        {
            var chunkId   = reader.ReadInt32();
            var chunkSize = reader.ReadInt32();

            if ( chunkId == DATA_CHUNK_ID )
            {
                header.DataSize = chunkSize;

                break;
            }

            reader.BaseStream.Seek(chunkSize, SeekOrigin.Current);

            if ( reader.BaseStream.Position >= reader.BaseStream.Length )
            {
                throw new InvalidDataException("Missing data chunk");
            }
        }

        return header;
    }

    public static bool ValidateWavFile(string filePath)
    {
        try
        {
            using (var reader = new BinaryReader(File.OpenRead(filePath)))
            {
                ReadWavHeader(reader);

                return true;
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private struct WavHeader
    {
        public int FileSize;

        public int Channels;

        public int SampleRate;

        public int ByteRate;

        public short BlockAlign;

        public short BitsPerSample;

        public int DataSize;
    }
}

using System.Numerics;

using Hexa.NET.ImGui;
using Hexa.NET.Utilities.Text;

using Silk.NET.Input;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     The main configuration editor component with improved architecture and performance
/// </summary>
public class ConfigEditorComponent : IRenderComponent
{
    private readonly IUiConfigurationManager _configManager;

    private readonly INotificationService _notificationService;

    private readonly IConfigSectionRegistry _sectionRegistry;

    private readonly IEditorStateManager _stateManager;

    private readonly IUiThemeManager _themeManager;

    private bool _isInitialized = false;

    public ConfigEditorComponent(
        IUiConfigurationManager configManager,
        IEditorStateManager     stateManager,
        IConfigSectionRegistry  sectionRegistry,
        IUiThemeManager         themeManager,
        INotificationService    notificationService)
    {
        _configManager       = configManager ?? throw new ArgumentNullException(nameof(configManager));
        _stateManager        = stateManager ?? throw new ArgumentNullException(nameof(stateManager));
        _sectionRegistry     = sectionRegistry ?? throw new ArgumentNullException(nameof(sectionRegistry));
        _themeManager        = themeManager ?? throw new ArgumentNullException(nameof(themeManager));
        _notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));

        // Subscribe to configuration change events
        _configManager.ConfigurationChanged += OnConfigurationChanged;

        // Subscribe to state change events
        _stateManager.StateChanged += OnStateChanged;
    }

    public void Dispose()
    {
        // Unsubscribe from events
        _configManager.ConfigurationChanged -= OnConfigurationChanged;
        _stateManager.StateChanged          -= OnStateChanged;

        // Dispose all registered section editors
        foreach ( var section in _sectionRegistry.GetSections() )
        {
            (section as IDisposable)?.Dispose();
        }

        GC.SuppressFinalize(this);
    }

    #region IRenderComponent Implementation

    public bool UseSpout => false;

    public string SpoutTarget => string.Empty;

    public int Priority => 0;

    public void Initialize(GL gl, IView view, IInputContext input)
    {
        if ( _isInitialized )
        {
            return;
        }

        // Apply global UI theme
        _themeManager.ApplyTheme();

        // Initialize all registered section editors
        foreach ( var section in _sectionRegistry.GetSections() )
        {
            section.Initialize();
        }

        _isInitialized = true;
    }

    public void Render(float deltaTime)
    {
        // Set window parameters
        // ImGui.SetNextWindowSize(new Vector2(900, 650), ImGuiCond.FirstUseEver);

        // RenderMenuBar();

        // Show notification banner if there are unsaved changes
        // if ( _stateManager.HasUnsavedChanges )
        // {
        //     RenderUnsavedChangesBanner();
        // }

        // Show notifications if present
        // RenderNotifications();

        // Render tab bar for sections
        RenderSectionTabs();
    }

    public void Update(float deltaTime)
    {
        // Update all registered section editors
        foreach ( var section in _sectionRegistry.GetSections() )
        {
            section.Update(deltaTime);
        }

        // Update notification service
        _notificationService.Update(deltaTime);
    }

    public void Resize()
    {
        // Not needed for this component
    }

    #endregion

    #region Private UI Methods

    private void RenderMenuBar()
    {
        if ( ImGui.BeginMenuBar() )
        {
            // Render section-specific menu items
            foreach ( var section in _sectionRegistry.GetSections() )
            {
                section.RenderMenuItems();
            }

            // Render active operations status on the right side
            RenderStatusIndicators();

            ImGui.EndMenuBar();
        }
    }

    private void RenderStatusIndicators()
    {
        var activeOperation = _stateManager.GetActiveOperation();
        if ( activeOperation != null )
        {
            var menuWidth = ImGui.GetWindowWidth();
            ImGui.SameLine(menuWidth - 160);

            // Display operation name
            ImGui.AlignTextToFramePadding();
            ImGui.Text(activeOperation.Name);

            // Animated dots
            var time = (int)(ImGui.GetTime() * 2) % 4;
            for ( var i = 0; i < time; i++ )
            {
                ImGui.SameLine(0, 2);
                ImGui.Text(".");
            }

            // Progress bar
            ImGui.SameLine();
            ImGui.SetNextItemWidth(100);
            ImGui.ProgressBar(activeOperation.Progress, new Vector2(100, 15), "");
        }
    }

    private void RenderUnsavedChangesBanner()
    {
        UiStyler.WithStyleColor(ImGuiCol.ChildBg, new Vector4(0.92f, 0.73f, 0.0f, 0.2f), () =>
                                                                                {
                                                                                UiStyler.WithStyleVar(ImGuiStyleVar.FramePadding, new Vector2(10, 5), () =>
                                                                                {
                                                                                ImGui.BeginChild("UnsavedChangesBar", new Vector2(ImGui.GetContentRegionAvail().X, 40), ImGuiChildFlags.Borders, ImGuiWindowFlags.NoScrollbar);

                                                                                ImGui.AlignTextToFramePadding();
                                                                                ImGui.TextColored(new Vector4(1, 0.7f, 0, 1), "You have unsaved changes!");

                                                                                ImGui.SameLine(ImGui.GetContentRegionAvail().X - 200);

                                                                                if ( ImGui.Button("Save Changes", new Vector2(100, 30)) )
                                                                                {
                                                                                SaveConfiguration();
                                                                                }

                                                                                ImGui.SameLine();

                                                                                if ( ImGui.Button("Discard", new Vector2(80, 30)) )
                                                                                {
                                                                                ReloadConfiguration();
                                                                                }

                                                                                ImGui.EndChild();
                                                                                });
                                                                                });
    }

    private void RenderNotifications()
    {
        foreach ( var notification in _notificationService.GetActiveNotifications() )
        {
            UiStyler.WithStyleColor(ImGuiCol.ChildBg, notification.GetBackgroundColor(), () =>
                                                                                {
                                                                                UiStyler.WithStyleVar(ImGuiStyleVar.FramePadding, new Vector2(10, 5), () =>
                                                                                {
                                                                                ImGui.BeginChild($"Notification_{notification.Id}", new Vector2(ImGui.GetContentRegionAvail().X, 40), ImGuiChildFlags.Borders, ImGuiWindowFlags.NoScrollbar);

                                                                                ImGui.AlignTextToFramePadding();
                                                                                ImGui.TextColored(notification.GetTextColor(), notification.Message);

                                                                                if ( notification.HasAction )
                                                                                {
                                                                                ImGui.SameLine(ImGui.GetContentRegionAvail().X - 100);

                                                                                if ( ImGui.Button(notification.ActionLabel, new Vector2(90, 30)) )
                                                                                {
                                                                                notification.InvokeAction();
                                                                                }
                                                                                }

                                                                                ImGui.EndChild();
                                                                                });
                                                                                });
        }
    }

    private unsafe void RenderSectionTabs()
    {
        var        sections   = _sectionRegistry.GetSections();
        const int  bufferSize = 256;
        var        buffer     = stackalloc byte[bufferSize];
        StrBuilder sb         = new(buffer, bufferSize);

        foreach ( var section in sections )
        {
            sb.Append("ScrollRegion");
            sb.Append("##");
            sb.Append(section.DisplayName);
            sb.End();

            ImGui.SetNextWindowSize(new Vector2(600, 0));
            if ( ImGui.Begin(section.DisplayName, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize) )
            {
                if ( ImGui.CollapsingHeader(section.DisplayName, ImGuiTreeNodeFlags.None) )
                {
                    var availWidth = ImGui.GetContentRegionAvail().X;
                    ImGui.BeginChild(sb, new Vector2(availWidth, 0), ImGuiChildFlags.AutoResizeY, ImGuiWindowFlags.HorizontalScrollbar);
                    section.Render();
                    ImGui.EndChild();
                }
            }

            ImGui.End();

            sb.Reset();
        }
    }

    #endregion

    #region Event Handlers

    private void OnConfigurationChanged(object sender, ConfigurationChangedEventArgs e)
    {
        // Notify relevant sections about configuration changes
        foreach ( var section in _sectionRegistry.GetSections() )
        {
            if ( section.SectionKey == e.SectionKey || e.SectionKey == null )
            {
                section.OnConfigurationChanged(e);
            }
        }
    }

    private void OnStateChanged(object sender, EditorStateChangedEventArgs e)
    {
        // Handle state changes that affect the main component
    }

    #endregion

    #region Helper Methods

    private void SaveConfiguration()
    {
        try
        {
            _configManager.SaveConfiguration();
            _notificationService.ShowSuccess("Configuration saved successfully");
        }
        catch (Exception ex)
        {
            _notificationService.ShowError($"Failed to save configuration: {ex.Message}");
        }
    }

    private void ReloadConfiguration()
    {
        try
        {
            _configManager.ReloadConfiguration();
            _notificationService.ShowInfo("Configuration reloaded");
        }
        catch (Exception ex)
        {
            _notificationService.ShowError($"Failed to reload configuration: {ex.Message}");
        }
    }

    #endregion
}

using System.Collections.Concurrent;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.Core.Conversation.Implementations.Context;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Session;

public class ConversationOrchestrator : IConversationOrchestrator
{
    private readonly ConcurrentDictionary<Guid, (IConversationSession Session, ValueTask RunTask)> _activeSessions = new();

    private readonly IOptionsMonitor<ConversationContextOptions> _conversationContextOptions;

    private readonly IOptions<ConversationOptions> _conversationOptions;

    private readonly ILogger<ConversationOrchestrator> _logger;

    private readonly CancellationTokenSource _orchestratorCts = new();

    private readonly IConversationSessionFactory _sessionFactory;

    public ConversationOrchestrator(ILogger<ConversationOrchestrator> logger, IConversationSessionFactory sessionFactory, IOptions<ConversationOptions> conversationOptions, IOptionsMonitor<ConversationContextOptions> conversationContextOptions)
    {
        _logger                     = logger;
        _sessionFactory             = sessionFactory;
        _conversationOptions        = conversationOptions;
        _conversationContextOptions = conversationContextOptions;
    }

    public async ValueTask DisposeAsync()
    {
        await StopAllSessionsAsync();

        _orchestratorCts.Dispose();

        var remainingIds = _activeSessions.Keys.ToList();
        if ( remainingIds.Count > 0 )
        {
            _logger.LogWarning("Found {Count} sessions remaining after StopAll. Force cleaning up.", remainingIds.Count);
            foreach ( var id in remainingIds )
            {
                if ( !_activeSessions.TryRemove(id, out var sessionInfo) )
                {
                    continue;
                }

                try
                {
                    await sessionInfo.Session.DisposeAsync();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error during final forced disposal of session {SessionId}.", id);
                }
            }
        }

        _activeSessions.Clear();

        GC.SuppressFinalize(this);
    }

    public IConversationSession GetSession(Guid sessionId)
    {
        if ( _activeSessions.TryGetValue(sessionId, out var sessionInfo) )
        {
            return sessionInfo.Session;
        }

        throw new KeyNotFoundException($"Session {sessionId} not found in active sessions.");
    }

    public async Task<Guid> StartNewSessionAsync(CancellationToken cancellationToken = default)
    {
        var sessionId = Guid.NewGuid();
        var context   = new ConversationContext([], _conversationContextOptions);
        var session   = _sessionFactory.CreateSession(context, _conversationOptions.Value, sessionId);

        try
        {
            var runTask = session.RunAsync(_orchestratorCts.Token);

            if ( _activeSessions.TryAdd(session.SessionId, (null!, ValueTask.CompletedTask)) )
            {
                _logger.LogInformation("Session {SessionId} started.", session.SessionId);

                var sessionJob = HandleSessionCompletionAsync(session, runTask);
                _activeSessions[session.SessionId] = (session, sessionJob);

                OnSessionsUpdated();

                return session.SessionId;
            }

            await session.StopAsync();
            await session.DisposeAsync();

            throw new InvalidOperationException($"Failed to add session {session.SessionId} to the active pool.");
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("Starting session {SessionId} was cancelled before RunAsync could start.", session.SessionId);
            await session.DisposeAsync();

            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to create or start session {SessionId}.", session.SessionId);
            await session.DisposeAsync();

            throw;
        }
    }

    public async ValueTask StopSessionAsync(Guid sessionId)
    {
        if ( _activeSessions.TryGetValue(sessionId, out var sessionInfo) )
        {
            try
            {
                await sessionInfo.Session.StopAsync();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error requesting stop for session {SessionId}.", sessionId);
            }
        }
        else
        {
            _logger.LogWarning("Session {SessionId} not found in active sessions for stopping.", sessionId);
        }
    }

    public IEnumerable<Guid> GetActiveSessionIds() { return _activeSessions.Keys.ToList(); }

    public async ValueTask StopAllSessionsAsync()
    {
        if ( !_orchestratorCts.IsCancellationRequested )
        {
            await _orchestratorCts.CancelAsync();
        }

        var activeSessionIds = _activeSessions.Keys.ToList(); // Get a snapshot of IDs

        foreach ( var sessionId in activeSessionIds )
        {
            if ( !_activeSessions.TryGetValue(sessionId, out var sessionInfo) )
            {
                continue;
            }

            try
            {
                await StopSessionAsync(sessionId);
                await sessionInfo.RunTask;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception occurred while waiting for sessions to stop during StopAllSessionsAsync.");
            }
        }
    }

    public event EventHandler? SessionsUpdated;

    private async ValueTask HandleSessionCompletionAsync(IConversationSession session, ValueTask runTask)
    {
        try
        {
            await runTask;
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Session {SessionId} RunAsync task was cancelled.", session.SessionId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Session {SessionId} RunAsync task completed with an unhandled exception.", session.SessionId);
        }
        finally
        {
            if ( _activeSessions.TryRemove(session.SessionId, out _) )
            {
                _logger.LogInformation("Session {SessionId} removed from active sessions.", session.SessionId);
                OnSessionsUpdated();
            }
            else
            {
                _logger.LogWarning("Session {SessionId} was already removed or not found during cleanup.", session.SessionId);
            }

            try
            {
                await session.DisposeAsync();
                _logger.LogDebug("Session {SessionId} disposed successfully.", session.SessionId);
            }
            catch (Exception disposeEx)
            {
                _logger.LogError(disposeEx, "Error disposing session {SessionId}.", session.SessionId);
            }
        }
    }

    protected virtual void OnSessionsUpdated() { SessionsUpdated?.Invoke(this, EventArgs.Empty); }
}

using System.Text.Json;

using PersonaEngine.Lib.Live2D.Framework;
using PersonaEngine.Lib.Live2D.Framework.Effect;
using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;
using PersonaEngine.Lib.Live2D.Framework.Motion;
using PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

namespace PersonaEngine.Lib.Live2D.App;

public class LAppModel : CubismUserModel
{
    /// <summary>
    ///     読み込まれている表情のリスト
    /// </summary>
    private readonly Dictionary<string, ACubismMotion> _expressions = [];

    /// <summary>
    ///     モデルに設定されたまばたき機能用パラメータID
    /// </summary>
    private readonly List<string> _eyeBlinkIds = [];

    private readonly LAppDelegate _lapp;

    /// <summary>
    ///     モデルに設定されたリップシンク機能用パラメータID
    /// </summary>
    private readonly List<string> _lipSyncIds = [];

    /// <summary>
    ///     モデルセッティングが置かれたディレクトリ
    /// </summary>
    private readonly string _modelHomeDir;

    /// <summary>
    ///     モデルセッティング情報
    /// </summary>
    private readonly ModelSettingObj _modelSetting;

    /// <summary>
    ///     読み込まれているモーションのリスト
    /// </summary>
    private readonly Dictionary<string, ACubismMotion> _motions = [];

    private readonly Random _random = new();

    /// <summary>
    ///     wavファイルハンドラ
    /// </summary>
    public LAppWavFileHandler _wavFileHandler = new();

    public static string GetMotionGroupName(string motionName)
    {
        var underscoreIndex = motionName.LastIndexOf('_');
        if ( underscoreIndex > 0 && int.TryParse(motionName.AsSpan(underscoreIndex + 1), out _) )
        {
            return motionName[..underscoreIndex];
        }

        return motionName; // Treat whole name as group if no "_Number" suffix
    }

    public        Action<LAppModel>? ValueUpdate;

    public LAppModel(LAppDelegate lapp, string dir, string fileName)
    {
        _lapp = lapp;

        if ( LAppDefine.MocConsistencyValidationEnable )
        {
            _mocConsistency = true;
        }

        IdParamAngleX = CubismFramework.CubismIdManager
                                       .GetId(CubismDefaultParameterId.ParamAngleX);

        IdParamAngleY = CubismFramework.CubismIdManager
                                       .GetId(CubismDefaultParameterId.ParamAngleY);

        IdParamAngleZ = CubismFramework.CubismIdManager.GetId(CubismDefaultParameterId.ParamAngleZ);
        IdParamBodyAngleX = CubismFramework.CubismIdManager
                                           .GetId(CubismDefaultParameterId.ParamBodyAngleX);

        IdParamEyeBallX = CubismFramework.CubismIdManager
                                         .GetId(CubismDefaultParameterId.ParamEyeBallX);

        IdParamEyeBallY = CubismFramework.CubismIdManager
                                         .GetId(CubismDefaultParameterId.ParamEyeBallY);

        _modelHomeDir = dir;

        CubismLog.Debug($"[Live2D]load model setting: {fileName}");

        using var stream = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

        _modelSetting = JsonSerializer.Deserialize(stream, ModelSettingObjContext.Default.ModelSettingObj)
                        ?? throw new Exception("model3.json error");

        Updating    = true;
        Initialized = false;

        //Cubism Model
        var path = _modelSetting.FileReferences?.Moc;
        if ( !string.IsNullOrWhiteSpace(path) )
        {
            path = Path.GetFullPath(_modelHomeDir + path);
            if ( !File.Exists(path) )
            {
                throw new Exception("model is null");
            }

            CubismLog.Debug($"[Live2D]create model: {path}");

            LoadModel(File.ReadAllBytes(path), _mocConsistency);
        }

        //Expression
        if ( _modelSetting.FileReferences?.Expressions?.Count > 0 )
        {
            for ( var i = 0; i < _modelSetting.FileReferences.Expressions.Count; i++ )
            {
                var item = _modelSetting.FileReferences.Expressions[i];
                var name = item.Name;
                path = item.File;
                path = Path.GetFullPath(_modelHomeDir + path);
                if ( !File.Exists(path) )
                {
                    continue;
                }

                var motion = new CubismExpressionMotion(path);

                if ( _expressions.ContainsKey(name) )
                {
                    _expressions[name] = motion;
                }
                else
                {
                    _expressions.Add(name, motion);
                }
            }
        }

        //Physics
        path = _modelSetting.FileReferences?.Physics;
        if ( !string.IsNullOrWhiteSpace(path) )
        {
            path = Path.GetFullPath(_modelHomeDir + path);
            if ( File.Exists(path) )
            {
                LoadPhysics(path);
            }
        }

        //Pose
        path = _modelSetting.FileReferences?.Pose;
        if ( !string.IsNullOrWhiteSpace(path) )
        {
            path = Path.GetFullPath(_modelHomeDir + path);
            if ( File.Exists(path) )
            {
                LoadPose(path);
            }
        }

        //EyeBlink
        if ( _modelSetting.IsExistEyeBlinkParameters() )
        {
            _eyeBlink = new CubismEyeBlink(_modelSetting);
        }

        LoadBreath();

        //UserData
        path = _modelSetting.FileReferences?.UserData;
        if ( !string.IsNullOrWhiteSpace(path) )
        {
            path = Path.GetFullPath(_modelHomeDir + path);
            if ( File.Exists(path) )
            {
                LoadUserData(path);
            }
        }

        // EyeBlinkIds
        if ( _eyeBlink != null )
        {
            _eyeBlinkIds.AddRange(_eyeBlink.ParameterIds);
        }

        // LipSyncIds
        if ( _modelSetting.IsExistLipSyncParameters() )
        {
            foreach ( var item in _modelSetting.Groups )
            {
                if ( item.Name == CubismModelSettingJson.LipSync )
                {
                    _lipSyncIds.AddRange(item.Ids);
                }
            }
        }

        //Layout
        Dictionary<string, float> layout = [];
        _modelSetting.GetLayoutMap(layout);
        ModelMatrix.SetupFromLayout(layout);

        Model.SaveParameters();

        if ( _modelSetting.FileReferences?.Motions?.Count > 0 )
        {
            foreach ( var item in _modelSetting.FileReferences.Motions )
            {
                PreloadMotionGroup(item.Key);
            }
        }

        _motionManager.StopAllMotions();

        Updating    = false;
        Initialized = true;

        CreateRenderer(new CubismRenderer_OpenGLES2(_lapp.GL, Model));

        SetupTextures();
    }

    public List<string> Motions => new(_motions.Keys);

    public List<string> Expressions => new(_expressions.Keys);

    public List<(string, int, float)> Parts
    {
        get
        {
            var list  = new List<(string, int, float)>();
            var count = Model.GetPartCount();
            for ( var a = 0; a < count; a++ )
            {
                list.Add((Model.GetPartId(a),
                          a, Model.GetPartOpacity(a)));
            }

            return list;
        }
    }

    public List<string> Parameters => new(Model.ParameterIds);

    /// <summary>
    ///     デルタ時間の積算値[秒]
    /// </summary>
    public float UserTimeSeconds { get; set; }

    public bool RandomMotion { get; set; } = true;

    public bool CustomValueUpdate { get; set; }

    /// <summary>
    ///     パラメータID: ParamAngleX
    /// </summary>
    public string IdParamAngleX { get; set; }

    /// <summary>
    ///     パラメータID: ParamAngleY
    /// </summary>
    public string IdParamAngleY { get; set; }

    /// <summary>
    ///     パラメータID: ParamAngleZ
    /// </summary>
    public string IdParamAngleZ { get; set; }

    /// <summary>
    ///     パラメータID: ParamBodyAngleX
    /// </summary>
    public string IdParamBodyAngleX { get; set; }

    /// <summary>
    ///     パラメータID: ParamEyeBallX
    /// </summary>
    public string IdParamEyeBallX { get; set; }

    /// <summary>
    ///     パラメータID: ParamEyeBallXY
    /// </summary>
    public string IdParamEyeBallY { get; set; }

    public string IdParamBreath { get; set; } = CubismFramework.CubismIdManager
                                                               .GetId(CubismDefaultParameterId.ParamBreath);

    public event Action<LAppModel, string>? Motion;

    public new void Dispose()
    {
        base.Dispose();

        _motions.Clear();
        _expressions.Clear();

        if ( _modelSetting.FileReferences?.Motions.Count > 0 )
        {
            foreach ( var item in _modelSetting.FileReferences.Motions )
            {
                ReleaseMotionGroup(item.Key);
            }
        }
    }

    public void LoadBreath()
    {
        //Breath
        _breath = new CubismBreath {
                                       Parameters = [
                                           new BreathParameterData {
                                                                       ParameterId = IdParamAngleX,
                                                                       Offset      = 0.0f,
                                                                       Peak        = 15.0f,
                                                                       Cycle       = 6.5345f,
                                                                       Weight      = 0.5f
                                                                   },
                                           new BreathParameterData {
                                                                       ParameterId = IdParamAngleY,
                                                                       Offset      = 0.0f,
                                                                       Peak        = 8.0f,
                                                                       Cycle       = 3.5345f,
                                                                       Weight      = 0.5f
                                                                   },
                                           new BreathParameterData {
                                                                       ParameterId = IdParamAngleZ,
                                                                       Offset      = 0.0f,
                                                                       Peak        = 10.0f,
                                                                       Cycle       = 5.5345f,
                                                                       Weight      = 0.5f
                                                                   },
                                           new BreathParameterData {
                                                                       ParameterId = IdParamBodyAngleX,
                                                                       Offset      = 0.0f,
                                                                       Peak        = 4.0f,
                                                                       Cycle       = 15.5345f,
                                                                       Weight      = 0.5f
                                                                   },
                                           new BreathParameterData {
                                                                       ParameterId = IdParamBreath,
                                                                       Offset      = 0.5f,
                                                                       Peak        = 0.5f,
                                                                       Cycle       = 3.2345f,
                                                                       Weight      = 0.5f
                                                                   }
                                       ]
                                   };
    }

    /// <summary>
    ///     レンダラを再構築する
    /// </summary>
    public void ReloadRenderer()
    {
        DeleteRenderer();

        CreateRenderer(new CubismRenderer_OpenGLES2(_lapp.GL, Model));

        SetupTextures();
    }

    /// <summary>
    ///     モデルの更新処理。モデルのパラメータから描画状態を決定する。
    /// </summary>
    public void Update()
    {
        var deltaTimeSeconds = LAppPal.DeltaTime;
        UserTimeSeconds += deltaTimeSeconds;

        _dragManager.Update(deltaTimeSeconds);
        _dragX = _dragManager.FaceX;
        _dragY = _dragManager.FaceY;

        // モーションによるパラメータ更新の有無
        var motionUpdated = false;

        //-----------------------------------------------------------------
        Model.LoadParameters(); // 前回セーブされた状態をロード
        if ( _motionManager.IsFinished() && RandomMotion )
        {
            // モーションの再生がない場合、待機モーションの中からランダムで再生する
            StartRandomMotion(LAppDefine.MotionGroupIdle, MotionPriority.PriorityIdle);
        }
        else
        {
            motionUpdated = _motionManager.UpdateMotion(Model, deltaTimeSeconds); // モーションを更新
        }

        Model.SaveParameters(); // 状態を保存

        //-----------------------------------------------------------------

        // 不透明度
        Opacity = Model.GetModelOpacity();

        // まばたき
        if ( !motionUpdated )
        {
            // メインモーションの更新がないとき
            // _eyeBlink?.UpdateParameters(Model, deltaTimeSeconds); // 目パチ
        }

        _expressionManager?.UpdateMotion(Model, deltaTimeSeconds); // 表情でパラメータ更新（相対変化）

        if ( CustomValueUpdate )
        {
            ValueUpdate?.Invoke(this);
        }
        else
        {
            //ドラッグによる変化
            //ドラッグによる顔の向きの調整
            Model.AddParameterValue(IdParamAngleX, _dragX * 30); // -30から30の値を加える
            Model.AddParameterValue(IdParamAngleY, _dragY * 30);
            Model.AddParameterValue(IdParamAngleZ, _dragX * _dragY * -30);

            //ドラッグによる体の向きの調整
            Model.AddParameterValue(IdParamBodyAngleX, _dragX * 10); // -10から10の値を加える

            //ドラッグによる目の向きの調整
            Model.AddParameterValue(IdParamEyeBallX, _dragX); // -1から1の値を加える
            Model.AddParameterValue(IdParamEyeBallY, _dragY);
        }

        // 呼吸など
        // _breath?.UpdateParameters(Model, deltaTimeSeconds);

        // 物理演算の設定
        _physics?.Evaluate(Model, deltaTimeSeconds);

        // リップシンクの設定
        // if ( _lipSync )
        // {
        //     // リアルタイムでリップシンクを行う場合、システムから音量を取得して0〜1の範囲で値を入力します。
        //     var value = 0.0f;
        //
        //     // 状態更新/RMS値取得
        //     _wavFileHandler.Update(deltaTimeSeconds);
        //     value = (float)_wavFileHandler.GetRms();
        //
        //     var weight = 3f; // Configure it as needed.
        //
        //     for ( var i = 0; i < _lipSyncIds.Count; ++i )
        //     {
        //         Model.SetParameterValue(_lipSyncIds[i], value * weight);
        //     }
        // }

        // ポーズの設定
        _pose?.UpdateParameters(Model, deltaTimeSeconds);

        Model.Update();
    }

    /// <summary>
    ///     モデルを描画する処理。モデルを描画する空間のView-Projection行列を渡す。
    /// </summary>
    /// <param name="matrix">View-Projection行列</param>
    public void Draw(CubismMatrix44 matrix)
    {
        if ( Model == null )
        {
            return;
        }

        matrix.MultiplyByMatrix(ModelMatrix);
        if ( Renderer is CubismRenderer_OpenGLES2 ren )
        {
            ren.ClearColor = _lapp.BGColor;
            ren.SetMvpMatrix(matrix);
        }

        DoDraw();
    }

    /// <summary>
    ///     Starts playing the motion specified by the argument.
    /// </summary>
    /// <param name="group">Motion Group Name</param>
    /// <param name="no">Number in group</param>
    /// <param name="priority">Priority</param>
    /// <param name="onFinishedMotionHandler">The callback function that is called when the motion playback ends. If NULL, it will not be called.</param>
    /// <returns>Returns the identification number of the started motion. Used as an argument for IsFinished() to determine whether an individual motion has finished. Returns "-1" if the motion cannot be started.</returns>
    public CubismMotionQueueEntry? StartMotion(string name, MotionPriority priority, FinishedMotionCallback? onFinishedMotionHandler = null)
    {
        var temp = name.Split("_");
        if ( temp.Length != 2 )
        {
            throw new Exception("motion name error");
        }

        return StartMotion(temp[0], int.Parse(temp[1]), priority, onFinishedMotionHandler);
    }

    public CubismMotionQueueEntry? StartMotion(string group, int no, MotionPriority priority, FinishedMotionCallback? onFinishedMotionHandler = null)
    {
        if ( priority == MotionPriority.PriorityForce )
        {
            _motionManager.ReservePriority = priority;
        }
        else if ( !_motionManager.ReserveMotion(priority) )
        {
            CubismLog.Debug("[Live2D]can't start motion.");

            return null;
        }

        var item = _modelSetting.FileReferences.Motions[group][no];

        //ex) idle_0
        var name = $"{group}_{no}";

        CubismMotion motion;
        if ( !_motions.TryGetValue(name, out var value) )
        {
            var path = item.File;
            path = Path.GetFullPath(_modelHomeDir + path);
            if ( !File.Exists(path) )
            {
                return null;
            }

            motion = new CubismMotion(path, onFinishedMotionHandler);
            var fadeTime = item.FadeInTime;
            if ( fadeTime >= 0.0f )
            {
                motion.FadeInSeconds = fadeTime;
            }

            fadeTime = item.FadeOutTime;
            if ( fadeTime >= 0.0f )
            {
                motion.FadeOutSeconds = fadeTime;
            }

            motion.SetEffectIds(_eyeBlinkIds, _lipSyncIds);
        }
        else
        {
            motion                  = (value as CubismMotion)!;
            motion.OnFinishedMotion = onFinishedMotionHandler;
        }

        //voice
        var voice = item.Sound;
        if ( !string.IsNullOrWhiteSpace(voice) )
        {
            var path = voice;
            path = _modelHomeDir + path;
            _wavFileHandler.Start(path);
        }

        CubismLog.Debug($"[Live2D]start motion: [{group}_{no}]");

        return _motionManager.StartMotionPriority(motion, priority);
    }

    /// <summary>
    ///     Starts playing a randomly selected motion.
    /// </summary>
    /// <param name="group">Motion Group Name</param>
    /// <param name="priority">Priority</param>
    /// <param name="onFinishedMotionHandler">A callback function that is called when the priority motion playback ends. If NULL, it will not be called.</param>
    /// <returns>Returns the identification number of the started motion. Used as an argument for IsFinished() to determine whether an individual motion has finished. Returns "-1" if the motion cannot be started.</returns>
    public CubismMotionQueueEntry? StartRandomMotion(string group, MotionPriority priority, FinishedMotionCallback? onFinishedMotionHandler = null)
    {
        if ( _modelSetting.FileReferences?.Motions?.ContainsKey(group) == true )
        {
            var no = _random.Next() % _modelSetting.FileReferences.Motions[group].Count;

            return StartMotion(group, no, priority, onFinishedMotionHandler);
        }

        return null;
    }

    public bool IsMotionFinished(CubismMotionQueueEntry entry)
    {
        return _motionManager.IsFinished(entry);
    }

    /// <summary>
    ///     Sets the facial motion specified by the argument.
    /// </summary>
    /// <param name="expressionID">Facial expression motion ID</param>
    public void SetExpression(string expressionID)
    {
        var motion = _expressions[expressionID];
        CubismLog.Debug($"[Live2D]expression: [{expressionID}]");

        if ( motion != null )
        {
            _expressionManager.StartMotionPriority(motion, MotionPriority.PriorityForce);
        }
        else
        {
            CubismLog.Debug($"[Live2D]expression[{expressionID}] is null ");
        }
    }

    /// <summary>
    ///     Set a randomly selected facial expression motion
    /// </summary>
    public void SetRandomExpression()
    {
        if ( _expressions.Count == 0 )
        {
            return;
        }

        var no = _random.Next() % _expressions.Count;
        var i  = 0;
        foreach ( var item in _expressions )
        {
            if ( i == no )
            {
                SetExpression(item.Key);

                return;
            }

            i++;
        }
    }

    /// <summary>
    ///     イベントの発火を受け取る
    /// </summary>
    /// <param name="eventValue"></param>
    protected override void MotionEventFired(string eventValue)
    {
        CubismLog.Debug($"[Live2D]{eventValue} is fired on LAppModel!!");
        Motion?.Invoke(this, eventValue);
    }

    /// <summary>
    ///     当たり判定テスト。
    ///     指定IDの頂点リストから矩形を計算し、座標が矩形範囲内か判定する。
    /// </summary>
    /// <param name="hitAreaName">当たり判定をテストする対象のID</param>
    /// <param name="x">判定を行うX座標</param>
    /// <param name="y">判定を行うY座標</param>
    /// <returns></returns>
    public bool HitTest(string hitAreaName, float x, float y)
    {
        // 透明時は当たり判定なし。
        if ( Opacity < 1 )
        {
            return false;
        }

        if ( _modelSetting.HitAreas?.Count > 0 )
        {
            for ( var i = 0; i < _modelSetting.HitAreas?.Count; i++ )
            {
                if ( _modelSetting.HitAreas[i].Name == hitAreaName )
                {
                    var id = CubismFramework.CubismIdManager.GetId(_modelSetting.HitAreas[i].Id);

                    return IsHit(id, x, y);
                }
            }
        }

        return false; // 存在しない場合はfalse
    }

    /// <summary>
    ///     モデルを描画する処理。モデルを描画する空間のView-Projection行列を渡す。
    /// </summary>
    protected void DoDraw()
    {
        if ( Model == null )
        {
            return;
        }

        (Renderer as CubismRenderer_OpenGLES2)?.DrawModel();
    }

    /// <summary>
    ///     モーションデータをグループ名から一括で解放する。
    ///     モーションデータの名前は内部でModelSettingから取得する。
    /// </summary>
    /// <param name="group">モーションデータのグループ名</param>
    private void ReleaseMotionGroup(string group)
    {
        var list = _modelSetting.FileReferences.Motions[group];
        for ( var i = 0; i < list.Count; i++ )
        {
            var voice = list[i].Sound;
        }
    }

    /// <summary>
    ///     OpenGLのテクスチャユニットにテクスチャをロードする
    /// </summary>
    private void SetupTextures()
    {
        if ( _modelSetting.FileReferences?.Textures?.Count > 0 )
        {
            for ( var modelTextureNumber = 0; modelTextureNumber < _modelSetting.FileReferences.Textures.Count; modelTextureNumber++ )
            {
                //OpenGLのテクスチャユニットにテクスチャをロードする
                var texturePath = _modelSetting.FileReferences.Textures[modelTextureNumber];
                if ( string.IsNullOrWhiteSpace(texturePath) )
                {
                    continue;
                }

                texturePath = Path.GetFullPath(_modelHomeDir + texturePath);

                var texture        = _lapp.TextureManager.CreateTextureFromPngFile(texturePath);
                var glTextueNumber = texture.ID;

                //OpenGL
                (Renderer as CubismRenderer_OpenGLES2)?.BindTexture(modelTextureNumber, glTextueNumber);
            }
        }
    }

    /// <summary>
    ///     モーションデータをグループ名から一括でロードする。
    ///     モーションデータの名前は内部でModelSettingから取得する。
    /// </summary>
    /// <param name="group">モーションデータのグループ名</param>
    private void PreloadMotionGroup(string group)
    {
        // グループに登録されているモーション数を取得
        var list = _modelSetting.FileReferences.Motions[group];

        for ( var i = 0; i < list.Count; i++ )
        {
            var item = list[i];
            //ex) idle_0
            // モーションのファイル名とパスの取得
            var name = $"{group}_{i}";
            var path = Path.GetFullPath(_modelHomeDir + item.File);

            // モーションデータの読み込み
            var tmpMotion = new CubismMotion(path);

            // フェードインの時間を取得
            var fadeTime = item.FadeInTime;
            if ( fadeTime >= 0.0f )
            {
                tmpMotion.FadeInSeconds = fadeTime;
            }

            // フェードアウトの時間を取得
            fadeTime = item.FadeOutTime;
            if ( fadeTime >= 0.0f )
            {
                tmpMotion.FadeOutSeconds = fadeTime;
            }

            tmpMotion.SetEffectIds(_eyeBlinkIds, _lipSyncIds);

            if ( _motions.ContainsKey(name) )
            {
                _motions[name] = tmpMotion;
            }
            else
            {
                _motions.Add(name, tmpMotion);
            }
        }
    }
}

using Microsoft.ML.OnnxRuntime;

namespace PersonaEngine.Lib.ASR.VAD;

internal class SileroVadOnnxModel : IDisposable
{
    private static readonly long[] sampleRateInput = [SileroConstants.SampleRate];

    private readonly long[] runningInputShape = [1, SileroConstants.BatchSize + SileroConstants.ContextSize];

    private readonly RunOptions runOptions;

    private readonly InferenceSession session;

    private readonly long[] stateShape = [2, 1, 128];

    public SileroVadOnnxModel(string modelPath)
    {
        var sessionOptions = new SessionOptions {
                                                    EnableMemoryPattern    = true,
                                                    ExecutionMode          = ExecutionMode.ORT_PARALLEL,
                                                    InterOpNumThreads      = 1,
                                                    IntraOpNumThreads      = 1,
                                                    GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
                                                    LogSeverityLevel       = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR
                                                };

        sessionOptions.AppendExecutionProvider_CUDA();

        session    = new InferenceSession(modelPath, sessionOptions);
        runOptions = new RunOptions();
    }

    public void Dispose() { session?.Dispose(); }

    public SileroInferenceState CreateInferenceState()
    {
        var state = new SileroInferenceState(session.CreateIoBinding());

        state.Binding.BindInput("state", OrtValue.CreateTensorValueFromMemory(state.State, stateShape));
        state.Binding.BindInput("sr", OrtValue.CreateTensorValueFromMemory(sampleRateInput, [1]));

        state.Binding.BindOutput("output", OrtValue.CreateTensorValueFromMemory(state.Output, [1, SileroConstants.OutputSize]));
        state.Binding.BindOutput("stateN", OrtValue.CreateTensorValueFromMemory(state.PendingState, stateShape));

        return state;
    }

    public float Call(Memory<float> input, SileroInferenceState state)
    {
        state.Binding.BindInput("input", OrtValue.CreateTensorValueFromMemory(OrtMemoryInfo.DefaultInstance, input, runningInputShape));
        // We need to swap the state and pending state to keep the state for the next inference
        // Zero allocation swap
        (state.State, state.PendingState) = (state.PendingState, state.State);

        session.RunWithBinding(runOptions, state.Binding);

        return state.Output[0];
    }
}

using System.Diagnostics;
using System.Numerics;

using Hexa.NET.ImGui;

using PersonaEngine.Lib.Audio;
using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Editor for Microphone section configuration.
///     Allows selection of the audio input device.
/// </summary>
public class MicrophoneConfigEditor : ConfigSectionEditorBase
{
    private const string DefaultDeviceDisplayName = "(Default Device)"; // Display name for null/empty device

    // --- Dependencies ---
    private readonly IMicrophone _microphone; // Dependency to get device list

    private List<string> _availableDevices = new();

    // --- State ---
    private MicrophoneConfiguration _currentConfig;

    private bool _loadingDevices = false;

    private string? _selectedDeviceName; // Can be null for default device

    /// <summary>
    ///     Initializes a new instance of the <see cref="MicrophoneConfigEditor" /> class.
    /// </summary>
    /// <param name="configManager">The configuration manager.</param>
    /// <param name="stateManager">The editor state manager.</param>
    /// <param name="microphone">The microphone service.</param>
    public MicrophoneConfigEditor(
        IUiConfigurationManager configManager,
        IEditorStateManager     stateManager,
        IMicrophone             microphone)
        : base(configManager, stateManager)
    {
        _microphone = microphone ?? throw new ArgumentNullException(nameof(microphone));

        // Load initial configuration
        LoadConfiguration();
    }

    // --- ConfigSectionEditorBase Implementation ---

    /// <summary>
    ///     Gets the key for the configuration section managed by this editor.
    /// </summary>
    public override string SectionKey => "Microphone"; // Or your specific key

    /// <summary>
    ///     Gets the display name for this editor section.
    /// </summary>
    public override string DisplayName => "Microphone Settings";

    /// <summary>
    ///     Initializes the editor, loading necessary data like available devices.
    /// </summary>
    public override void Initialize()
    {
        LoadAvailableDevices(); // Load devices on initialization
    }

    /// <summary>
    ///     Renders the ImGui UI for the microphone configuration.
    /// </summary>
    public override void Render()
    {
        var availWidth = ImGui.GetContentRegionAvail().X;

        // --- Device Selection Section ---
        ImGui.Spacing();
        ImGui.SeparatorText("Input Device Selection");
        ImGui.Spacing();

        ImGui.BeginGroup(); // Group controls for alignment
        {
            ImGui.AlignTextToFramePadding();
            ImGui.Text("Input Device:");
            ImGui.SameLine(120); // Adjust spacing as needed

            // Combo box for device selection
            ImGui.SetNextItemWidth(availWidth - 120 - 100); // Width calculation: Available - Label - Refresh Button

            var currentSelectionDisplay = string.IsNullOrEmpty(_selectedDeviceName)
                                              ? DefaultDeviceDisplayName
                                              : _selectedDeviceName;

            if ( _loadingDevices )
            {
                // Show loading state
                ImGui.BeginDisabled();
                var loadingText = "Loading devices...";
                // Use InputText as a visual placeholder during loading
                ImGui.InputText("##DeviceLoading", ref loadingText, 100, ImGuiInputTextFlags.ReadOnly);
                ImGui.EndDisabled();
            }
            else
            {
                // Actual combo box
                if ( ImGui.BeginCombo("##DeviceSelector", currentSelectionDisplay) )
                {
                    // Add "(Default Device)" option first
                    var isDefaultSelected = string.IsNullOrEmpty(_selectedDeviceName);
                    if ( ImGui.Selectable(DefaultDeviceDisplayName, isDefaultSelected) )
                    {
                        if ( _selectedDeviceName != null ) // Check if changed
                        {
                            _selectedDeviceName = null;
                            UpdateConfiguration();
                        }
                    }

                    if ( isDefaultSelected )
                    {
                        ImGui.SetItemDefaultFocus();
                    }

                    // Add actual devices if available
                    if ( _availableDevices.Count == 0 && !_loadingDevices )
                    {
                        ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 1.0f), "No input devices found.");
                    }
                    else
                    {
                        foreach ( var device in _availableDevices )
                        {
                            var isSelected = device == _selectedDeviceName;
                            if ( ImGui.Selectable(device, isSelected) )
                            {
                                if ( _selectedDeviceName != device ) // Check if changed
                                {
                                    _selectedDeviceName = device;
                                    UpdateConfiguration();
                                }
                            }

                            if ( isSelected )
                            {
                                ImGui.SetItemDefaultFocus();
                            }
                        }
                    }

                    ImGui.EndCombo();
                }
            }

            // Refresh Button
            ImGui.SameLine(0, 10);                                  // Add spacing before button
            if ( ImGui.Button("Refresh##Dev", new Vector2(80, 0)) ) // Unique ID for button
            {
                LoadAvailableDevices();
            }

            if ( ImGui.IsItemHovered() )
            {
                ImGui.SetTooltip("Refresh the list of available input devices.");
            }
        }

        ImGui.EndGroup(); // End device selection group

        ImGui.Spacing();
        ImGui.Separator();
        ImGui.Spacing();

        // --- Action Buttons ---
        // Center the buttons roughly
        float buttonWidth      = 150;
        var   totalButtonWidth = buttonWidth * 2 + 10; // Two buttons + spacing
        var   initialPadding   = (availWidth - totalButtonWidth) * 0.5f;
        if ( initialPadding < 0 )
        {
            initialPadding = 0;
        }

        ImGui.SetCursorPosX(initialPadding);

        // Reset Button
        if ( ImGui.Button("Reset", new Vector2(buttonWidth, 0)) )
        {
            ResetToDefaults();
        }

        if ( ImGui.IsItemHovered() )
        {
            ImGui.SetTooltip("Reset microphone settings to default values.");
        }

        ImGui.SameLine(0, 10); // Spacing between buttons

        // Save Button (Disabled if no changes)
        var hasChanges = StateManager.HasUnsavedChanges; // Check unsaved state
        if ( !hasChanges )
        {
            ImGui.BeginDisabled();
        }

        if ( ImGui.Button("Save", new Vector2(buttonWidth, 0)) )
        {
            SaveConfiguration();
        }

        if ( ImGui.IsItemHovered() && hasChanges )
        {
            ImGui.SetTooltip("Save the current microphone settings.");
        }

        if ( !hasChanges )
        {
            ImGui.EndDisabled();
        }
    }

    /// <summary>
    ///     Updates the editor state (currently unused for this simple editor).
    /// </summary>
    /// <param name="deltaTime">Time elapsed since the last frame.</param>
    public override void Update(float deltaTime)
    {
        // No per-frame update logic needed for this editor yet
    }

    /// <summary>
    ///     Handles configuration changes, reloading if necessary.
    /// </summary>
    /// <param name="args">Event arguments containing change details.</param>
    public override void OnConfigurationChanged(ConfigurationChangedEventArgs args)
    {
        base.OnConfigurationChanged(args); // Call base implementation

        // Reload configuration if the source indicates a full reload
        if ( args.Type == ConfigurationChangedEventArgs.ChangeType.Reloaded )
        {
            LoadConfiguration();
            // Optionally reload devices if the config might affect them,
            // though usually device list is independent of config.
            // LoadAvailableDevices();
        }
    }

    /// <summary>
    ///     Disposes resources used by the editor (currently none specific).
    /// </summary>
    public override void Dispose()
    {
        // Unsubscribe from events if any were added
        base.Dispose(); // Call base implementation
    }

    // --- Configuration Management ---

    /// <summary>
    ///     Loads the microphone configuration from the configuration manager.
    /// </summary>
    private void LoadConfiguration()
    {
        _currentConfig = ConfigManager.GetConfiguration<MicrophoneConfiguration>(SectionKey)
                         ?? new MicrophoneConfiguration(); // Get or create default

        // Update local state from loaded config
        _selectedDeviceName = _currentConfig.DeviceName;

        // Ensure the UI reflects the loaded state without marking as changed initially
        MarkAsSaved(); // Reset change tracking after loading
    }

    /// <summary>
    ///     Fetches the list of available microphone devices.
    /// </summary>
    private void LoadAvailableDevices()
    {
        if ( _loadingDevices )
        {
            return; // Prevent concurrent loading
        }

        try
        {
            _loadingDevices = true;
            // Consider adding an ActiveOperation to StateManager if this takes time
            // var operation = new ActiveOperation("load-mic-devices", "Loading Microphones");
            // StateManager.RegisterActiveOperation(operation);

            // Get devices using the injected service
            _availableDevices = _microphone.GetAvailableDevices().ToList();

            // Ensure the currently selected device still exists
            if ( !string.IsNullOrEmpty(_selectedDeviceName) &&
                 !_availableDevices.Contains(_selectedDeviceName) )
            {
                Debug.WriteLine($"Warning: Configured microphone '{_selectedDeviceName}' not found. Reverting to default.");
                // Optionally notify the user here
                _selectedDeviceName = null; // Revert to default
                UpdateConfiguration();      // Update the config state
            }

            // StateManager.ClearActiveOperation(operation.Id);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error loading microphone devices: {ex.Message}");
            _availableDevices = new List<string>(); // Clear list on error
            // Optionally show an error message to the user via ImGui or logging
        }
        finally
        {
            _loadingDevices = false;
        }
    }

    /// <summary>
    ///     Updates the configuration object and notifies the manager.
    /// </summary>
    private void UpdateConfiguration()
    {
        // Create the updated configuration record
        var updatedConfig = _currentConfig with // Use record 'with' expression
                            {
                                DeviceName = _selectedDeviceName
                            };

        // Check if the configuration actually changed before updating
        if ( !_currentConfig.Equals(updatedConfig) )
        {
            _currentConfig = updatedConfig;
            ConfigManager.UpdateConfiguration(_currentConfig, SectionKey);
            MarkAsChanged(); // Mark that there are unsaved changes
        }
    }

    /// <summary>
    ///     Saves the current configuration changes.
    /// </summary>
    private void SaveConfiguration()
    {
        ConfigManager.SaveConfiguration();
        MarkAsSaved(); // Mark changes as saved
    }

    /// <summary>
    ///     Resets the microphone configuration to default values.
    /// </summary>
    private void ResetToDefaults()
    {
        var defaultConfig = new MicrophoneConfiguration(); // Create default config

        // Update local state
        _selectedDeviceName = defaultConfig.DeviceName; // Should be null

        // Update configuration only if it differs from current
        if ( !_currentConfig.Equals(defaultConfig) )
        {
            _currentConfig = defaultConfig;
            ConfigManager.UpdateConfiguration(_currentConfig, SectionKey);
            MarkAsChanged(); // Mark changes needing save
        }
        else
        {
            // If already default, just ensure saved state is correct
            MarkAsSaved();
        }
    }
}

namespace PersonaEngine.Lib.TTS.RVC;

public class ACFMethod : IF0Predictor
{
    private readonly int SampleRate;

    private int HopLength;

    public ACFMethod(int hopLength, int samplingRate)
    {
        HopLength  = hopLength;
        SampleRate = samplingRate;
    }

    public void ComputeF0(ReadOnlyMemory<float> wav, Memory<float> f0Output, int length)
    {
        HopLength = (int)Math.Floor(wav.Length / (double)length);

        var wavSpan     = wav.Span;
        var f0Span      = f0Output.Span;
        var frameLength = HopLength;

        for ( var i = 0; i < length; i++ )
        {
            // Create a window for this frame without allocations
            var startIdx  = i * frameLength;
            var endIdx    = Math.Min(startIdx + (int)(frameLength * 1.5), wav.Length);
            var frameSize = endIdx - startIdx;

            f0Span[i] = ComputeF0ForFrame(wavSpan.Slice(startIdx, frameSize));
        }
    }

    public void Dispose() { }

    private float ComputeF0ForFrame(ReadOnlySpan<float> frame)
    {
        var         n               = frame.Length;
        Span<float> autocorrelation = stackalloc float[n]; // Use stack allocation to avoid heap allocations

        // Calculate autocorrelation function
        for ( var lag = 0; lag < n; lag++ )
        {
            float sum = 0;
            for ( var i = 0; i < n - lag; i++ )
            {
                sum += frame[i] * frame[i + lag];
            }

            autocorrelation[lag] = sum;
        }

        // Ignore zero-delay peak, find first non-zero delay peak
        var peakIndex = 1;
        var maxVal    = autocorrelation[1];
        for ( ; peakIndex < autocorrelation.Length && (maxVal = autocorrelation[peakIndex]) > 0; peakIndex++ )
        {
            ;
        }

        for ( var lag = peakIndex; lag < n; lag++ )
        {
            if ( autocorrelation[lag] > maxVal )
            {
                maxVal    = autocorrelation[lag];
                peakIndex = lag;
            }
        }

        // Calculate fundamental frequency
        var f0 = SampleRate / (float)peakIndex;

        return f0;
    }
}

using System.Text;
using System.Text.RegularExpressions;

namespace PersonaEngine.Lib.TTS.Synthesis;

public class PhonemizerG2P : IPhonemizer
{
    private readonly IFallbackPhonemizer? _fallback;

    private readonly ILexicon _lexicon;

    private readonly Regex _linkRegex;

    private readonly IPosTagger _posTagger;

    private readonly Regex _subtokenRegex;

    private readonly string _unk;

    private bool _disposed;

    public PhonemizerG2P(IPosTagger posTagger, ILexicon lexicon, IFallbackPhonemizer? fallback = null, string unk = "❓")
    {
        _posTagger = posTagger ?? throw new ArgumentNullException(nameof(posTagger));
        _fallback  = fallback;
        _unk       = unk;
        _lexicon   = lexicon;

        // Initialize regular expressions once for performance
        _linkRegex = new Regex(@"\[([^\]]+)\]\(([^\)]*)\)", RegexOptions.Compiled);
        _subtokenRegex = new Regex(
                                   @"^[''']+|\p{Lu}(?=\p{Lu}\p{Ll})|(?:^-)?(?:\d?[,.]?\d)+|[-_]+|[''']{2,}|\p{L}*?(?:[''']\p{L})*?\p{Ll}(?=\p{Lu})|\p{L}+(?:[''']\p{L})*|[^-_\p{L}'''\d]|[''']+$",
                                   RegexOptions.Compiled);

        _disposed = false;
    }

    public async Task<PhonemeResult> ToPhonemesAsync(string text, CancellationToken cancellationToken = default)
    {
        // 1. Preprocess text
        var (processedText, textTokens, features) = Preprocess(text);

        // 2. Tag text with POS tagger
        var posTokens = await _posTagger.TagAsync(processedText, cancellationToken);

        // 3. Convert to internal token representation
        var tokens = ConvertToTokens(posTokens);

        // 4. Apply features from preprocessing
        ApplyFeatures(tokens, textTokens, features);

        // 5. Fold left (merge non-head tokens with previous token)
        tokens = FoldLeft(tokens);

        // 6. Retokenize (split complex tokens and handle special cases)
        var retokenizedTokens = Retokenize(tokens);

        // 7. Process phonemes using lexicon and fallback
        var ctx = new TokenContext();
        await ProcessTokensAsync(retokenizedTokens, ctx, cancellationToken);

        // 8. Merge retokenized tokens
        var mergedTokens = MergeRetokenizedTokens(retokenizedTokens);

        // 9. Generate final phoneme string
        var phonemes = string.Concat(mergedTokens.Select(t => (t.Phonemes ?? _unk) + t.Whitespace));

        return new PhonemeResult(phonemes, mergedTokens);
    }

    public void Dispose() { GC.SuppressFinalize(this); }

    private List<Token> ConvertToTokens(IReadOnlyList<PosToken> posTokens)
    {
        var tokens = new List<Token>(posTokens.Count);

        foreach ( var pt in posTokens )
        {
            tokens.Add(new Token { Text = pt.Text, Tag = pt.PartOfSpeech ?? string.Empty, Whitespace = pt.IsWhitespace ? " " : string.Empty });
        }

        return tokens;
    }

    private (string ProcessedText, List<string> Tokens, Dictionary<int, object> Features) Preprocess(string text)
    {
        text = text.TrimStart();
        var result   = new StringBuilder(text.Length);
        var tokens   = new List<string>();
        var features = new Dictionary<int, object>();

        var lastEnd = 0;

        foreach ( Match m in _linkRegex.Matches(text) )
        {
            if ( m.Index > lastEnd )
            {
                var segment = text.Substring(lastEnd, m.Index - lastEnd);
                result.Append(segment);
                tokens.AddRange(segment.Split(' ', StringSplitOptions.RemoveEmptyEntries));
            }

            var linkText    = m.Groups[1].Value;
            var featureText = m.Groups[2].Value;

            object? featureValue = null;
            if ( featureText.Length >= 1 )
            {
                if ( (featureText[0] == '-' || featureText[0] == '+') &&
                     int.TryParse(featureText, out var intVal) )
                {
                    featureValue = intVal;
                }
                else if ( int.TryParse(featureText, out intVal) )
                {
                    featureValue = intVal;
                }
                else if ( featureText == "0.5" || featureText == "+0.5" )
                {
                    featureValue = 0.5;
                }
                else if ( featureText == "-0.5" )
                {
                    featureValue = -0.5;
                }
                else if ( featureText.Length > 1 )
                {
                    var firstChar = featureText[0];
                    var lastChar  = featureText[featureText.Length - 1];

                    if ( firstChar == '/' && lastChar == '/' )
                    {
                        featureValue = firstChar + featureText.Substring(1, featureText.Length - 2);
                    }
                    else if ( firstChar == '#' && lastChar == '#' )
                    {
                        featureValue = firstChar + featureText.Substring(1, featureText.Length - 2);
                    }
                }
            }

            if ( featureValue != null )
            {
                features[tokens.Count] = featureValue;
            }

            result.Append(linkText);
            tokens.Add(linkText);
            lastEnd = m.Index + m.Length;
        }

        if ( lastEnd < text.Length )
        {
            var segment = text.Substring(lastEnd);
            result.Append(segment);
            tokens.AddRange(segment.Split(' ', StringSplitOptions.RemoveEmptyEntries));
        }

        return (result.ToString(), tokens, features);
    }

    private void ApplyFeatures(List<Token> tokens, List<string> textTokens, Dictionary<int, object> features)
    {
        if ( features.Count == 0 )
        {
            return;
        }

        var alignment = CreateBidirectionalAlignment(textTokens, tokens.Select(t => t.Text).ToList());

        foreach ( var kvp in features )
        {
            var sourceIndex = kvp.Key;
            var value       = kvp.Value;

            var matchedIndices = new List<int>();
            for ( var i = 0; i < alignment.Length; i++ )
            {
                if ( alignment[i] == sourceIndex )
                {
                    matchedIndices.Add(i);
                }
            }

            for ( var matchCount = 0; matchCount < matchedIndices.Count; matchCount++ )
            {
                var tokenIndex = matchedIndices[matchCount];

                if ( tokenIndex >= tokens.Count )
                {
                    continue;
                }

                if ( value is int intValue )
                {
                    tokens[tokenIndex].Stress = intValue;
                }
                else if ( value is double doubleValue )
                {
                    tokens[tokenIndex].Stress = doubleValue;
                }
                else if ( value is string strValue )
                {
                    if ( strValue.StartsWith("/") )
                    {
                        // The "i == 0" in Python refers to the first match of this feature
                        tokens[tokenIndex].IsHead   = matchCount == 0;
                        tokens[tokenIndex].Phonemes = matchCount == 0 ? strValue.Substring(1) : string.Empty;
                        tokens[tokenIndex].Rating   = 5;
                    }
                    else if ( strValue.StartsWith("#") )
                    {
                        tokens[tokenIndex].NumFlags = strValue.Substring(1);
                    }
                }
            }
        }
    }

    private int[] CreateBidirectionalAlignment(List<string> source, List<string> target)
    {
        // This is a simplified version - ideally it would match spaCy's algorithm more closely
        var alignment = new int[target.Count];

        // Create a combined string from each list to verify they match overall
        var sourceText = string.Join("", source).ToLowerInvariant();
        var targetText = string.Join("", target).ToLowerInvariant();

        // Initialize all alignments to no match
        for ( var i = 0; i < alignment.Length; i++ )
        {
            alignment[i] = -1;
        }

        var targetIndex = 0;
        var sourcePos   = 0;

        // Track position in the joined strings to handle multi-token to single token mappings
        for ( var sourceIndex = 0; sourceIndex < source.Count; sourceIndex++ )
        {
            var sourceToken = source[sourceIndex].ToLowerInvariant();
            sourcePos += sourceToken.Length;

            var targetPos = 0;
            for ( var i = 0; i < targetIndex; i++ )
            {
                targetPos += target[i].ToLowerInvariant().Length;
            }

            // Map all target tokens that overlap with this source token
            while ( targetIndex < target.Count && targetPos < sourcePos )
            {
                var targetToken = target[targetIndex].ToLowerInvariant();
                alignment[targetIndex] = sourceIndex;

                targetPos += targetToken.Length;
                targetIndex++;
            }
        }

        return alignment;
    }

    private List<Token> FoldLeft(List<Token> tokens)
    {
        if ( tokens.Count <= 1 )
        {
            return tokens;
        }

        var result = new List<Token>(tokens.Count);
        result.Add(tokens[0]);

        for ( var i = 1; i < tokens.Count; i++ )
        {
            if ( !tokens[i].IsHead )
            {
                var merged = Token.MergeTokens(
                                               [result[^1], tokens[i]],
                                               _unk);

                result[^1] = merged;
            }
            else
            {
                result.Add(tokens[i]);
            }
        }

        return result;
    }

    private List<object> Retokenize(List<Token> tokens)
    {
        var     result          = new List<object>(tokens.Count * 2); // Estimate capacity
        string? currentCurrency = null;

        for ( var i = 0; i < tokens.Count; i++ )
        {
            var         token = tokens[i];
            List<Token> subtokens;

            // Split token if needed
            if ( token.Alias == null && token.Phonemes == null )
            {
                var subTokenTexts = Subtokenize(token.Text);
                subtokens = new List<Token>(subTokenTexts.Count);

                for ( var j = 0; j < subTokenTexts.Count; j++ )
                {
                    subtokens.Add(new Token {
                                                Text       = subTokenTexts[j],
                                                Tag        = token.Tag,
                                                Whitespace = j == subTokenTexts.Count - 1 ? token.Whitespace : string.Empty,
                                                IsHead     = token.IsHead && j == 0,
                                                Stress     = token.Stress,
                                                NumFlags   = token.NumFlags
                                            });
                }
            }
            else
            {
                subtokens = new List<Token> { token };
            }

            // Process each subtoken
            for ( var j = 0; j < subtokens.Count; j++ )
            {
                var t = subtokens[j];

                if ( t.Alias != null || t.Phonemes != null )
                {
                    // Skip special handling for already processed tokens
                }
                else if ( t.Tag == "$" && PhonemizerConstants.Currencies.ContainsKey(t.Text) )
                {
                    currentCurrency = t.Text;
                    t.Phonemes      = string.Empty;
                    t.Rating        = 4;
                }
                else if ( t is { Tag: ":", Text: "-" or "–" } )
                {
                    t.Phonemes = "—";
                    t.Rating   = 3;
                }
                else if ( PhonemizerConstants.PunctTags.Contains(t.Tag) )
                {
                    if ( PhonemizerConstants.PunctTagPhonemes.TryGetValue(t.Tag, out var phoneme) )
                    {
                        t.Phonemes = phoneme;
                    }
                    else
                    {
                        var sb = new StringBuilder();
                        foreach ( var c in t.Text )
                        {
                            if ( PhonemizerConstants.Puncts.Contains(c) )
                            {
                                sb.Append(c);
                            }
                        }

                        t.Phonemes = sb.ToString();
                    }

                    t.Rating = 4;
                }
                else if ( currentCurrency != null )
                {
                    if ( t.Tag != "CD" )
                    {
                        currentCurrency = null;
                    }
                    else if ( j + 1 == subtokens.Count && (i + 1 == tokens.Count || tokens[i + 1].Tag != "CD") )
                    {
                        t.Currency = currentCurrency;
                    }
                }
                else if ( 0 < j && j < subtokens.Count - 1 &&
                          t.Text == "2" &&
                          char.IsLetter(subtokens[j - 1].Text[subtokens[j - 1].Text.Length - 1]) &&
                          char.IsLetter(subtokens[j + 1].Text[0]) )
                {
                    t.Alias = "to";
                }

                // Add to result
                if ( t.Alias != null || t.Phonemes != null )
                {
                    result.Add(t);
                }
                else if ( result.Count > 0 &&
                          result[result.Count - 1] is List<Token> lastList &&
                          lastList.Count > 0 &&
                          string.IsNullOrEmpty(lastList[lastList.Count - 1].Whitespace) )
                {
                    t.IsHead = false;
                    ((List<Token>)result[^1]).Add(t);
                }
                else
                {
                    result.Add(string.IsNullOrEmpty(t.Whitespace) ? new List<Token> { t } : t);
                }
            }
        }

        // Simplify lists with single elements
        for ( var i = 0; i < result.Count; i++ )
        {
            if ( result[i] is List<Token> list && list.Count == 1 )
            {
                result[i] = list[0];
            }
        }

        return result;
    }

    private List<string> Subtokenize(string word)
    {
        var matches = _subtokenRegex.Matches(word);
        if ( matches.Count == 0 )
        {
            return new List<string> { word };
        }

        var result = new List<string>(matches.Count);
        foreach ( Match match in matches )
        {
            result.Add(match.Value);
        }

        return result;
    }

    private async Task ProcessTokensAsync(List<object> tokens, TokenContext ctx, CancellationToken cancellationToken)
    {
        // Process tokens in reverse order
        for ( var i = tokens.Count - 1; i >= 0; i-- )
        {
            if ( cancellationToken.IsCancellationRequested )
            {
                return;
            }

            if ( tokens[i] is Token token )
            {
                if ( token.Phonemes == null )
                {
                    (token.Phonemes, token.Rating) = await GetPhonemesAsync(token, ctx, cancellationToken);
                }

                ctx = TokenContext.UpdateContext(ctx, token.Phonemes, token);
            }
            else if ( tokens[i] is List<Token> subtokens )
            {
                await ProcessSubtokensAsync(subtokens, ctx, cancellationToken);
            }
        }
    }

    private async Task ProcessSubtokensAsync(List<Token> tokens, TokenContext ctx, CancellationToken cancellationToken)
    {
        int left  = 0,
            right = tokens.Count;

        var shouldFallback = false;

        while ( left < right )
        {
            if ( cancellationToken.IsCancellationRequested )
            {
                return;
            }

            if ( tokens.Skip(left).Take(right - left).Any(t => t.Alias != null || t.Phonemes != null) )
            {
                left++;

                continue;
            }

            var mergedToken = Token.MergeTokens(tokens.Skip(left).Take(right - left).ToList(), _unk);
            var (phonemes, rating) = await GetPhonemesAsync(mergedToken, ctx, cancellationToken);

            if ( phonemes != null )
            {
                tokens[left].Phonemes = phonemes;
                tokens[left].Rating   = rating;

                for ( var i = left + 1; i < right; i++ )
                {
                    tokens[i].Phonemes = string.Empty;
                    tokens[i].Rating   = rating;
                }

                ctx   = TokenContext.UpdateContext(ctx, phonemes, mergedToken);
                right = left;
                left  = 0;
            }
            else if ( left + 1 < right )
            {
                left++;
            }
            else
            {
                right--;
                var t = tokens[right];

                if ( t.Phonemes == null )
                {
                    if ( t.Text.All(c => PhonemizerConstants.SubtokenJunks.Contains(c)) )
                    {
                        t.Phonemes = string.Empty;
                        t.Rating   = 3;
                    }
                    else if ( _fallback != null )
                    {
                        shouldFallback = true;

                        break;
                    }
                }

                left = 0;
            }
        }

        if ( shouldFallback && _fallback != null )
        {
            var mergedToken = Token.MergeTokens(tokens, _unk);
            var (phonemes, rating) = await _fallback.GetPhonemesAsync(mergedToken.Text, cancellationToken);

            if ( phonemes != null )
            {
                tokens[0].Phonemes = phonemes;
                tokens[0].Rating   = rating;

                for ( var j = 1; j < tokens.Count; j++ )
                {
                    tokens[j].Phonemes = string.Empty;
                    tokens[j].Rating   = rating;
                }
            }
        }

        ResolveTokens(tokens);
    }

    private async Task<(string? Phonemes, int? Rating)> GetPhonemesAsync(Token token, TokenContext ctx, CancellationToken cancellationToken)
    {
        // Try to get from lexicon
        var (phonemes, rating) = _lexicon.ProcessToken(token, ctx);

        // If not found, try fallback
        if ( phonemes == null && _fallback != null )
        {
            return await _fallback.GetPhonemesAsync(token.Alias ?? token.Text, cancellationToken);
        }

        return (phonemes, rating);
    }

    private void ResolveTokens(List<Token> tokens)
    {
        // Calculate if there should be space between phonemes
        var text = string.Concat(tokens.Take(tokens.Count - 1).Select(t => t.Text + t.Whitespace)) +
                   tokens[tokens.Count - 1].Text;

        var prespace = text.Contains(' ') || text.Contains('/') ||
                       text.Where(c => !PhonemizerConstants.SubtokenJunks.Contains(c))
                           .Select(c => char.IsLetter(c)
                                            ? 0
                                            : char.IsDigit(c)
                                                ? 1
                                                : 2)
                           .Distinct()
                           .Count() > 1;

        // Handle specific cases
        for ( var i = 0; i < tokens.Count; i++ )
        {
            var t = tokens[i];

            if ( t.Phonemes == null )
            {
                if ( i == tokens.Count - 1 && t.Text.Length > 0 &&
                     PhonemizerConstants.NonQuotePuncts.Contains(t.Text[0]) )
                {
                    t.Phonemes = t.Text;
                    t.Rating   = 3;
                }
                else if ( t.Text.All(c => PhonemizerConstants.SubtokenJunks.Contains(c)) )
                {
                    t.Phonemes = string.Empty;
                    t.Rating   = 3;
                }
            }
            else if ( i > 0 )
            {
                t.Prespace = prespace;
            }
        }

        if ( prespace )
        {
            return;
        }

        // Adjust stress patterns
        var indices = new List<(bool HasPrimaryStress, int Weight, int Index)>();
        for ( var i = 0; i < tokens.Count; i++ )
        {
            if ( !string.IsNullOrEmpty(tokens[i].Phonemes) )
            {
                var hasPrimary = tokens[i].Phonemes.Contains(PhonemizerConstants.PrimaryStress);
                indices.Add((hasPrimary, tokens[i].StressWeight(), i));
            }
        }

        if ( indices.Count == 2 && tokens[indices[0].Index].Text.Length == 1 )
        {
            var i = indices[1].Index;
            tokens[i].Phonemes = ApplyStress(tokens[i].Phonemes!, -0.5);

            return;
        }

        if ( indices.Count < 2 || indices.Count(x => x.HasPrimaryStress) <= (indices.Count + 1) / 2 )
        {
            return;
        }

        indices.Sort();
        foreach ( var (_, _, i) in indices.Take(indices.Count / 2) )
        {
            tokens[i].Phonemes = ApplyStress(tokens[i].Phonemes!, -0.5);
        }
    }

    private string ApplyStress(string phonemes, double stress)
    {
        if ( stress < -1 )
        {
            return phonemes
                   .Replace(PhonemizerConstants.PrimaryStress.ToString(), string.Empty)
                   .Replace(PhonemizerConstants.SecondaryStress.ToString(), string.Empty);
        }

        if ( stress == -1 || ((stress == 0 || stress == -0.5) && phonemes.Contains(PhonemizerConstants.PrimaryStress)) )
        {
            return phonemes
                   .Replace(PhonemizerConstants.SecondaryStress.ToString(), string.Empty)
                   .Replace(PhonemizerConstants.PrimaryStress.ToString(), PhonemizerConstants.SecondaryStress.ToString());
        }

        if ( (stress == 0 || stress == 0.5 || stress == 1) &&
             !phonemes.Contains(PhonemizerConstants.PrimaryStress) &&
             !phonemes.Contains(PhonemizerConstants.SecondaryStress) )
        {
            if ( !phonemes.Any(c => PhonemizerConstants.Vowels.Contains(c)) )
            {
                return phonemes;
            }

            return RestressPhonemes(PhonemizerConstants.SecondaryStress + phonemes);
        }

        if ( stress >= 1 &&
             !phonemes.Contains(PhonemizerConstants.PrimaryStress) &&
             phonemes.Contains(PhonemizerConstants.SecondaryStress) )
        {
            return phonemes.Replace(PhonemizerConstants.SecondaryStress.ToString(), PhonemizerConstants.PrimaryStress.ToString());
        }

        if ( stress > 1 &&
             !phonemes.Contains(PhonemizerConstants.PrimaryStress) &&
             !phonemes.Contains(PhonemizerConstants.SecondaryStress) )
        {
            if ( !phonemes.Any(c => PhonemizerConstants.Vowels.Contains(c)) )
            {
                return phonemes;
            }

            return RestressPhonemes(PhonemizerConstants.PrimaryStress + phonemes);
        }

        return phonemes;
    }

    private string RestressPhonemes(string phonemes)
    {
        var chars         = phonemes.ToCharArray();
        var charPositions = new List<(int Position, char Char)>();

        for ( var i = 0; i < chars.Length; i++ )
        {
            charPositions.Add((i, chars[i]));
        }

        var stressPositions = new Dictionary<int, int>();
        for ( var i = 0; i < charPositions.Count; i++ )
        {
            if ( PhonemizerConstants.Stresses.Contains(charPositions[i].Char) )
            {
                // Find the next vowel
                var vowelPos = -1;
                for ( var j = i + 1; j < charPositions.Count; j++ )
                {
                    if ( PhonemizerConstants.Vowels.Contains(charPositions[j].Char) )
                    {
                        vowelPos = j;

                        break;
                    }
                }

                if ( vowelPos != -1 )
                {
                    stressPositions[charPositions[i].Position] = charPositions[vowelPos].Position;
                    charPositions[i]                           = ((int)(vowelPos - 0.5), charPositions[i].Char);
                }
            }
        }

        charPositions.Sort((a, b) => a.Position.CompareTo(b.Position));

        return new string(charPositions.Select(cp => cp.Char).ToArray());
    }

    private List<Token> MergeRetokenizedTokens(List<object> retokenizedTokens)
    {
        var result = new List<Token>();

        foreach ( var item in retokenizedTokens )
        {
            if ( item is Token token )
            {
                result.Add(token);
            }
            else if ( item is List<Token> tokens )
            {
                result.Add(Token.MergeTokens(tokens, _unk));
            }
        }

        return result;
    }
}

using System.Drawing;

using FontStashSharp;

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.UI.Common;
using PersonaEngine.Lib.UI.Text.Rendering;

using Silk.NET.OpenGL;

using StbImageSharp;

using Texture = PersonaEngine.Lib.UI.Common.Texture;

namespace PersonaEngine.Lib.UI;

/// <summary>
///     Manages caching and loading of fonts and textures.
/// </summary>
public class FontProvider : IStartupTask
{
    private const string FONTS_DIR = @"Resources\Fonts";

    private const string IMAGES_PATH = @"Resources\Imgs";

    private readonly Dictionary<string, FontSystem> _fontCache = new();

    private readonly ILogger<FontProvider> _logger;

    private readonly Dictionary<string, Texture> _textureCache = new();

    private Texture2DManager _texture2DManager = null!;

    // static FontProvider()
    // {
    //     FontSystemDefaults.FontLoader = new SixLaborsFontLoader();
    // }

    public FontProvider(ILogger<FontProvider> logger) { _logger = logger; }

    public void Execute(GL gl) { _texture2DManager = new Texture2DManager(gl); }

    public Task<IReadOnlyList<string>> GetAvailableFontsAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            if ( !Directory.Exists(FONTS_DIR) )
            {
                _logger.LogWarning("Fonts directory not found: {Path}", FONTS_DIR);

                return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
            }

            var fontFiles = Directory.GetFiles(FONTS_DIR, "*.ttf");
            var fontNames = new List<string>(fontFiles.Length);
            foreach ( var file in fontFiles )
            {
                var voiceId = Path.GetFileName(file);
                fontNames.Add(voiceId);
            }

            _logger.LogInformation("Found {Count} available fonts", fontNames.Count);

            return Task.FromResult<IReadOnlyList<string>>(fontNames);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting available fonts");

            throw;
        }
    }

    public FontSystem GetFontSystem(string fontName)
    {
        if ( !_fontCache.TryGetValue(fontName, out var fontSystem) )
        {
            fontSystem = new FontSystem();
            var fontData = File.ReadAllBytes(Path.Combine(FONTS_DIR, fontName));
            fontSystem.AddFont(fontData);
            _fontCache[fontName] = fontSystem;
        }

        return fontSystem;
    }

    public Texture GetTexture(string imageName)
    {
        if ( !_textureCache.TryGetValue(imageName, out var texture) )
        {
            using var stream      = File.OpenRead(Path.Combine(IMAGES_PATH, imageName));
            var       imageResult = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);

            // Premultiply alpha
            unsafe
            {
                fixed (byte* b = imageResult.Data)
                {
                    var ptr = b;
                    for ( var i = 0; i < imageResult.Data.Length; i += 4, ptr += 4 )
                    {
                        var falpha = ptr[3] / 255.0f;
                        ptr[0] = (byte)(ptr[0] * falpha);
                        ptr[1] = (byte)(ptr[1] * falpha);
                        ptr[2] = (byte)(ptr[2] * falpha);
                    }
                }
            }

            texture = (Texture)_texture2DManager.CreateTexture(imageResult.Width, imageResult.Height);
            _texture2DManager.SetTextureData(texture, new Rectangle(0, 0, (int)texture.Width, (int)texture.Height), imageResult.Data);
            _textureCache[imageName] = texture;
        }

        return texture;
    }
}

using PersonaEngine.Lib.Configuration;

using Silk.NET.OpenGL;

using Spout.Interop;

namespace PersonaEngine.Lib.UI.Spout;

/// <summary>
///     Provides real-time frame sharing via Spout with custom framebuffer support.
/// </summary>
public class SpoutManager : IDisposable
{
    private readonly SpoutConfiguration _config;

    private readonly GL _gl;

    private readonly SpoutSender _spoutSender;

    private uint _colorAttachment;

    private uint _customFbo;

    private bool _customFboInitialized = false;

    private uint _depthAttachment;

    public SpoutManager(GL gl, SpoutConfiguration config)
    {
        _gl          = gl;
        _config      = config;
        _spoutSender = new SpoutSender();

        InitializeCustomFramebuffer();

        if ( !_spoutSender.CreateSender(config.OutputName, (uint)_config.Width, (uint)_config.Height, 0) )
        {
            _spoutSender.Dispose();
            Console.WriteLine($"Failed to create Spout Sender '{config.OutputName}'.");
        }
    }

    public void Dispose()
    {
        if ( _customFboInitialized )
        {
            _gl.DeleteTexture(_colorAttachment);
            _gl.DeleteTexture(_depthAttachment);
            _gl.DeleteFramebuffer(_customFbo);
        }

        _spoutSender?.Dispose();
    }

    private unsafe void InitializeCustomFramebuffer()
    {
        _customFbo = _gl.GenFramebuffer();
        _gl.BindFramebuffer(GLEnum.Framebuffer, _customFbo);

        _colorAttachment = _gl.GenTexture();
        _gl.BindTexture(GLEnum.Texture2D, _colorAttachment);
        _gl.TexImage2D(GLEnum.Texture2D, 0, (int)GLEnum.Rgba8,
                       (uint)_config.Width, (uint)_config.Height, 0, GLEnum.Rgba, GLEnum.UnsignedByte, null);

        _gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)GLEnum.Linear);
        _gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)GLEnum.Linear);
        _gl.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0,
                                 GLEnum.Texture2D, _colorAttachment, 0);

        _depthAttachment = _gl.GenTexture();
        _gl.BindTexture(GLEnum.Texture2D, _depthAttachment);
        _gl.TexImage2D(GLEnum.Texture2D, 0, (int)GLEnum.DepthComponent,
                       (uint)_config.Width, (uint)_config.Height, 0, GLEnum.DepthComponent, GLEnum.Float, null);

        _gl.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.DepthAttachment,
                                 GLEnum.Texture2D, _depthAttachment, 0);

        if ( _gl.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete )
        {
            Console.WriteLine("Custom framebuffer is not complete!");

            return;
        }

        _gl.BindFramebuffer(GLEnum.Framebuffer, 0);

        _customFboInitialized = true;
    }

    /// <summary>
    ///     Begins rendering to the custom framebuffer
    /// </summary>
    public void BeginFrame()
    {
        if ( _customFboInitialized )
        {
            _gl.BindFramebuffer(GLEnum.Framebuffer, _customFbo);

            _gl.Viewport(0, 0, (uint)_config.Width, (uint)_config.Height);

            var blendEnabled = _gl.IsEnabled(EnableCap.Blend);

            // Enable blending for transparency
            _gl.Enable(EnableCap.Blend);
            _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);

            // Clear with transparency (RGBA: 0,0,0,0)
            _gl.ClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            _gl.Clear((uint)(GLEnum.ColorBufferBit | GLEnum.DepthBufferBit));

            if ( !blendEnabled )
            {
                _gl.Disable(EnableCap.Blend);
            }
        }
    }

    /// <summary>
    ///     Sends the current frame to Spout and returns to the default framebuffer
    /// </summary>
    /// <param name="blitToScreen">Whether to copy the framebuffer to the screen</param>
    /// <param name="windowWidth">Window width if blitting to screen</param>
    /// <param name="windowHeight">Window height if blitting to screen</param>
    public void SendFrame(bool blitToScreen = true, int windowWidth = 0, int windowHeight = 0)
    {
        if ( _customFboInitialized )
        {
            _gl.GetInteger(GetPName.UnpackAlignment, out var previousUnpackAlignment);
            _gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);

            _spoutSender.SendFbo(_customFbo, (uint)_config.Width, (uint)_config.Height, true);

            _gl.PixelStore(PixelStoreParameter.UnpackAlignment, previousUnpackAlignment);

            if ( blitToScreen && windowWidth > 0 && windowHeight > 0 )
            {
                _gl.BindFramebuffer(GLEnum.ReadFramebuffer, _customFbo);
                _gl.BindFramebuffer(GLEnum.DrawFramebuffer, 0); // Default framebuffer

                var blendEnabled = _gl.IsEnabled(EnableCap.Blend);
                if ( !blendEnabled )
                {
                    _gl.Enable(EnableCap.Blend);
                    _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
                }

                _gl.BlitFramebuffer(
                                    0, 0, _config.Width, _config.Height,
                                    0, 0, windowWidth, windowHeight,
                                    (uint)GLEnum.ColorBufferBit,
                                    GLEnum.Linear);

                if ( !blendEnabled )
                {
                    _gl.Disable(EnableCap.Blend);
                }
            }

            _gl.BindFramebuffer(GLEnum.Framebuffer, 0);
        }
        else
        {
            _gl.GetInteger(GetPName.ReadFramebufferBinding, out var fboId);
            _spoutSender.SendFbo((uint)fboId, (uint)_config.Width, (uint)_config.Height, true);
        }
    }

    /// <summary>
    ///     Updates the custom framebuffer dimensions if needed
    /// </summary>
    public void ResizeFramebuffer(int width, int height)
    {
        // if ( _config.Width == width && _config.Height == height )
        // {
        //     return;
        // }

        // _config.Width  = width;
        // _config.Height = height;

        // if ( _customFboInitialized )
        // {
        //     _gl.DeleteTexture(_colorAttachment);
        //     _gl.DeleteTexture(_depthAttachment);
        //     _gl.DeleteFramebuffer(_customFbo);
        //     _customFboInitialized = false;
        // }
        //
        // InitializeCustomFramebuffer();
        //
        // _spoutSender.UpdateSender(_config.OutputName, (uint)width, (uint)height);
    }
}

// <auto-generated>
//     This file was generated by uniffi-bindgen-cs v0.8.4+v0.25.0
//     See https://github.com/NordSecurity/uniffi-bindgen-cs for more information.
// </auto-generated>

#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace uniffi.rust_lib;

// This is a helper for safely working with byte buffers returned from the Rust code.
// A rust-owned buffer is represented by its capacity, its current length, and a
// pointer to the underlying data.

[StructLayout(LayoutKind.Sequential)]
internal struct RustBuffer {
    public int capacity;
    public int len;
    public IntPtr data;

    public static RustBuffer Alloc(int size) {
        return _UniffiHelpers.RustCall((ref RustCallStatus status) => {
            var buffer = _UniFFILib.ffi_rust_lib_rustbuffer_alloc(size, ref status);
            if (buffer.data == IntPtr.Zero) {
                throw new AllocationException($"RustBuffer.Alloc() returned null data pointer (size={size})");
            }
            return buffer;
        });
    }

    public static void Free(RustBuffer buffer) {
        _UniffiHelpers.RustCall((ref RustCallStatus status) => {
            _UniFFILib.ffi_rust_lib_rustbuffer_free(buffer, ref status);
        });
    }

    public static BigEndianStream MemoryStream(IntPtr data, int length) {
        unsafe {
            return new BigEndianStream(new UnmanagedMemoryStream((byte*)data.ToPointer(), length));
        }
    }

    public BigEndianStream AsStream() {
        unsafe {
            return new BigEndianStream(new UnmanagedMemoryStream((byte*)data.ToPointer(), len));
        }
    }

    public BigEndianStream AsWriteableStream() {
        unsafe {
            return new BigEndianStream(new UnmanagedMemoryStream((byte*)data.ToPointer(), capacity, capacity, FileAccess.Write));
        }
    }
}

// This is a helper for safely passing byte references into the rust code.
// It's not actually used at the moment, because there aren't many things that you
// can take a direct pointer to managed memory, and if we're going to copy something
// then we might as well copy it into a `RustBuffer`. But it's here for API
// completeness.

[StructLayout(LayoutKind.Sequential)]
internal struct ForeignBytes {
    public int length;
    public IntPtr data;
}

// The FfiConverter interface handles converter types to and from the FFI
//
// All implementing objects should be public to support external types.  When a
// type is external we need to import it's FfiConverter.
internal abstract class FfiConverter<CsType, FfiType> {
    // Convert an FFI type to a C# type
    public abstract CsType Lift(FfiType value);

    // Convert C# type to an FFI type
    public abstract FfiType Lower(CsType value);

    // Read a C# type from a `ByteBuffer`
    public abstract CsType Read(BigEndianStream stream);

    // Calculate bytes to allocate when creating a `RustBuffer`
    //
    // This must return at least as many bytes as the write() function will
    // write. It can return more bytes than needed, for example when writing
    // Strings we can't know the exact bytes needed until we the UTF-8
    // encoding, so we pessimistically allocate the largest size possible (3
    // bytes per codepoint).  Allocating extra bytes is not really a big deal
    // because the `RustBuffer` is short-lived.
    public abstract int AllocationSize(CsType value);

    // Write a C# type to a `ByteBuffer`
    public abstract void Write(CsType value, BigEndianStream stream);

    // Lower a value into a `RustBuffer`
    //
    // This method lowers a value into a `RustBuffer` rather than the normal
    // FfiType.  It's used by the callback interface code.  Callback interface
    // returns are always serialized into a `RustBuffer` regardless of their
    // normal FFI type.
    public RustBuffer LowerIntoRustBuffer(CsType value) {
        var rbuf = RustBuffer.Alloc(AllocationSize(value));
        try {
            var stream = rbuf.AsWriteableStream();
            Write(value, stream);
            rbuf.len = Convert.ToInt32(stream.Position);
            return rbuf;
        } catch {
            RustBuffer.Free(rbuf);
            throw;
        }
    }

    // Lift a value from a `RustBuffer`.
    //
    // This here mostly because of the symmetry with `lowerIntoRustBuffer()`.
    // It's currently only used by the `FfiConverterRustBuffer` class below.
    protected CsType LiftFromRustBuffer(RustBuffer rbuf) {
        var stream = rbuf.AsStream();
        try {
           var item = Read(stream);
           if (stream.HasRemaining()) {
               throw new InternalException("junk remaining in buffer after lifting, something is very wrong!!");
           }
           return item;
        } finally {
            RustBuffer.Free(rbuf);
        }
    }
}

// FfiConverter that uses `RustBuffer` as the FfiType
internal abstract class FfiConverterRustBuffer<CsType>: FfiConverter<CsType, RustBuffer> {
    public override CsType Lift(RustBuffer value) {
        return LiftFromRustBuffer(value);
    }
    public override RustBuffer Lower(CsType value) {
        return LowerIntoRustBuffer(value);
    }
}

// A handful of classes and functions to support the generated data structures.
// This would be a good candidate for isolating in its own ffi-support lib.
// Error runtime.
[StructLayout(LayoutKind.Sequential)]
struct RustCallStatus {
    public sbyte code;
    public RustBuffer error_buf;

    public bool IsSuccess() {
        return code == 0;
    }

    public bool IsError() {
        return code == 1;
    }

    public bool IsPanic() {
        return code == 2;
    }
}

// Base class for all uniffi exceptions
internal class UniffiException: System.Exception {
    public UniffiException(): base() {}
    public UniffiException(string message): base(message) {}
}

internal class UndeclaredErrorException: UniffiException {
    public UndeclaredErrorException(string message): base(message) {}
}

internal class PanicException: UniffiException {
    public PanicException(string message): base(message) {}
}

internal class AllocationException: UniffiException {
    public AllocationException(string message): base(message) {}
}

internal class InternalException: UniffiException {
    public InternalException(string message): base(message) {}
}

internal class InvalidEnumException: InternalException {
    public InvalidEnumException(string message): base(message) {
    }
}

internal class UniffiContractVersionException: UniffiException {
    public UniffiContractVersionException(string message): base(message) {
    }
}

internal class UniffiContractChecksumException: UniffiException {
    public UniffiContractChecksumException(string message): base(message) {
    }
}

// Each top-level error class has a companion object that can lift the error from the call status's rust buffer
interface CallStatusErrorHandler<E> where E: System.Exception {
    E Lift(RustBuffer error_buf);
}

// CallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR
class NullCallStatusErrorHandler: CallStatusErrorHandler<UniffiException> {
    public static NullCallStatusErrorHandler INSTANCE = new NullCallStatusErrorHandler();

    public UniffiException Lift(RustBuffer error_buf) {
        RustBuffer.Free(error_buf);
        return new UndeclaredErrorException("library has returned an error not declared in UNIFFI interface file");
    }
}

// Helpers for calling Rust
// In practice we usually need to be synchronized to call this safely, so it doesn't
// synchronize itself
class _UniffiHelpers {
    public delegate void RustCallAction(ref RustCallStatus status);
    public delegate U RustCallFunc<out U>(ref RustCallStatus status);

    // Call a rust function that returns a Result<>.  Pass in the Error class companion that corresponds to the Err
    public static U RustCallWithError<U, E>(CallStatusErrorHandler<E> errorHandler, RustCallFunc<U> callback)
        where E: UniffiException
    {
        var status = new RustCallStatus();
        var return_value = callback(ref status);
        if (status.IsSuccess()) {
            return return_value;
        } else if (status.IsError()) {
            throw errorHandler.Lift(status.error_buf);
        } else if (status.IsPanic()) {
            // when the rust code sees a panic, it tries to construct a rustbuffer
            // with the message.  but if that code panics, then it just sends back
            // an empty buffer.
            if (status.error_buf.len > 0) {
                throw new PanicException(FfiConverterString.INSTANCE.Lift(status.error_buf));
            } else {
                throw new PanicException("Rust panic");
            }
        } else {
            throw new InternalException($"Unknown rust call status: {status.code}");
        }
    }

    // Call a rust function that returns a Result<>.  Pass in the Error class companion that corresponds to the Err
    public static void RustCallWithError<E>(CallStatusErrorHandler<E> errorHandler, RustCallAction callback)
        where E: UniffiException
    {
        _UniffiHelpers.RustCallWithError(errorHandler, (ref RustCallStatus status) => {
            callback(ref status);
            return 0;
        });
    }

    // Call a rust function that returns a plain value
    public static U RustCall<U>(RustCallFunc<U> callback) {
        return _UniffiHelpers.RustCallWithError(NullCallStatusErrorHandler.INSTANCE, callback);
    }

    // Call a rust function that returns a plain value
    public static void RustCall(RustCallAction callback) {
        _UniffiHelpers.RustCall((ref RustCallStatus status) => {
            callback(ref status);
            return 0;
        });
    }
}

static class FFIObjectUtil {
    public static void DisposeAll(params Object?[] list) {
        foreach (var obj in list) {
            Dispose(obj);
        }
    }

    // Dispose is implemented by recursive type inspection at runtime. This is because
    // generating correct Dispose calls for recursive complex types, e.g. List<List<int>>
    // is quite cumbersome.
    private static void Dispose(dynamic? obj) {
        if (obj == null) {
            return;
        }

        if (obj is IDisposable disposable) {
            disposable.Dispose();
            return;
        }

        var type = obj.GetType();
        if (type != null) {
            if (type.IsGenericType) {
                if (type.GetGenericTypeDefinition().IsAssignableFrom(typeof(List<>))) {
                    foreach (var value in obj) {
                        Dispose(value);
                    }
                } else if (type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>))) {
                    foreach (var value in obj.Values) {
                        Dispose(value);
                    }
                }
            }
        }
    }
}

// Big endian streams are not yet available in dotnet :'(
// https://github.com/dotnet/runtime/issues/26904

class StreamUnderflowException: System.Exception {
    public StreamUnderflowException() {
    }
}

class BigEndianStream {
    Stream stream;
    public BigEndianStream(Stream stream) {
        this.stream = stream;
    }

    public bool HasRemaining() {
        return (stream.Length - stream.Position) > 0;
    }

    public long Position {
        get => stream.Position;
        set => stream.Position = value;
    }

    public void WriteBytes(byte[] value) {
        stream.Write(value, 0, value.Length);
    }

    public void WriteByte(byte value) {
        stream.WriteByte(value);
    }

    public void WriteUShort(ushort value) {
        stream.WriteByte((byte)(value >> 8));
        stream.WriteByte((byte)value);
    }

    public void WriteUInt(uint value) {
        stream.WriteByte((byte)(value >> 24));
        stream.WriteByte((byte)(value >> 16));
        stream.WriteByte((byte)(value >> 8));
        stream.WriteByte((byte)value);
    }

    public void WriteULong(ulong value) {
        WriteUInt((uint)(value >> 32));
        WriteUInt((uint)value);
    }

    public void WriteSByte(sbyte value) {
        stream.WriteByte((byte)value);
    }

    public void WriteShort(short value) {
        WriteUShort((ushort)value);
    }

    public void WriteInt(int value) {
        WriteUInt((uint)value);
    }

    public void WriteFloat(float value) {
        unsafe {
            WriteInt(*((int*)&value));
        }
    }

    public void WriteLong(long value) {
        WriteULong((ulong)value);
    }

    public void WriteDouble(double value) {
        WriteLong(BitConverter.DoubleToInt64Bits(value));
    }

    public byte[] ReadBytes(int length) {
        CheckRemaining(length);
        byte[] result = new byte[length];
        stream.Read(result, 0, length);
        return result;
    }

    public byte ReadByte() {
        CheckRemaining(1);
        return Convert.ToByte(stream.ReadByte());
    }

    public ushort ReadUShort() {
        CheckRemaining(2);
        return (ushort)(stream.ReadByte() << 8 | stream.ReadByte());
    }

    public uint ReadUInt() {
        CheckRemaining(4);
        return (uint)(stream.ReadByte() << 24
            | stream.ReadByte() << 16
            | stream.ReadByte() << 8
            | stream.ReadByte());
    }

    public ulong ReadULong() {
        return (ulong)ReadUInt() << 32 | (ulong)ReadUInt();
    }

    public sbyte ReadSByte() {
        return (sbyte)ReadByte();
    }

    public short ReadShort() {
        return (short)ReadUShort();
    }

    public int ReadInt() {
        return (int)ReadUInt();
    }

    public float ReadFloat() {
        unsafe {
            int value = ReadInt();
            return *((float*)&value);
        }
    }

    public long ReadLong() {
        return (long)ReadULong();
    }

    public double ReadDouble() {
        return BitConverter.Int64BitsToDouble(ReadLong());
    }

    private void CheckRemaining(int length) {
        if (stream.Length - stream.Position < length) {
            throw new StreamUnderflowException();
        }
    }
}

// Contains loading, initialization code,
// and the FFI Function declarations in a com.sun.jna.Library.

// This is an implementation detail which will be called internally by the public API.
static class _UniFFILib {
    static _UniFFILib() {
        _UniFFILib.uniffiCheckContractApiVersion();
        _UniFFILib.uniffiCheckApiChecksums();

        }

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_capture_fullscreen(ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_capture_monitor(RustBuffer @name,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_capture_rect(uint @x,uint @y,uint @width,uint @height,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_capture_window(uint @x,uint @y,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_capture_window_by_title(RustBuffer @name,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_get_monitor(uint @x,uint @y,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_get_primary_monitor(ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_get_screen_dimensions(RustBuffer @name,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer uniffi_rust_lib_fn_func_get_working_area(ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer ffi_rust_lib_rustbuffer_alloc(int @size,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer ffi_rust_lib_rustbuffer_from_bytes(ForeignBytes @bytes,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rustbuffer_free(RustBuffer @buf,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer ffi_rust_lib_rustbuffer_reserve(RustBuffer @buf,int @additional,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_continuation_callback_set(IntPtr @callback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_u8(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_u8(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_u8(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern byte ffi_rust_lib_rust_future_complete_u8(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_i8(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_i8(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_i8(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern sbyte ffi_rust_lib_rust_future_complete_i8(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_u16(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_u16(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_u16(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern ushort ffi_rust_lib_rust_future_complete_u16(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_i16(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_i16(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_i16(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern short ffi_rust_lib_rust_future_complete_i16(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_u32(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_u32(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_u32(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern uint ffi_rust_lib_rust_future_complete_u32(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_i32(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_i32(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_i32(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern int ffi_rust_lib_rust_future_complete_i32(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_u64(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_u64(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_u64(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern ulong ffi_rust_lib_rust_future_complete_u64(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_i64(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_i64(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_i64(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern long ffi_rust_lib_rust_future_complete_i64(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_f32(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_f32(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_f32(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern float ffi_rust_lib_rust_future_complete_f32(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_f64(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_f64(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_f64(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern double ffi_rust_lib_rust_future_complete_f64(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_pointer(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_pointer(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_pointer(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern IntPtr ffi_rust_lib_rust_future_complete_pointer(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_rust_buffer(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_rust_buffer(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_rust_buffer(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern RustBuffer ffi_rust_lib_rust_future_complete_rust_buffer(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_poll_void(IntPtr @handle,IntPtr @uniffiCallback
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_cancel_void(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_free_void(IntPtr @handle
    );

    [DllImport("rust_lib")]
    public static extern void ffi_rust_lib_rust_future_complete_void(IntPtr @handle,ref RustCallStatus _uniffi_out_err
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_capture_fullscreen(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_capture_monitor(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_capture_rect(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_capture_window(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_capture_window_by_title(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_get_monitor(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_get_primary_monitor(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_get_screen_dimensions(
    );

    [DllImport("rust_lib")]
    public static extern ushort uniffi_rust_lib_checksum_func_get_working_area(
    );

    [DllImport("rust_lib")]
    public static extern uint ffi_rust_lib_uniffi_contract_version(
    );

    static void uniffiCheckContractApiVersion() {
        var scaffolding_contract_version = _UniFFILib.ffi_rust_lib_uniffi_contract_version();
        if (24 != scaffolding_contract_version) {
            throw new UniffiContractVersionException($"uniffi.rust_lib: uniffi bindings expected version `24`, library returned `{scaffolding_contract_version}`");
        }
    }

    static void uniffiCheckApiChecksums() {
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_capture_fullscreen();
            if (checksum != 57650) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_capture_fullscreen` checksum `57650`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_capture_monitor();
            if (checksum != 7847) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_capture_monitor` checksum `7847`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_capture_rect();
            if (checksum != 28248) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_capture_rect` checksum `28248`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_capture_window();
            if (checksum != 33381) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_capture_window` checksum `33381`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_capture_window_by_title();
            if (checksum != 11081) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_capture_window_by_title` checksum `11081`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_get_monitor();
            if (checksum != 29961) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_get_monitor` checksum `29961`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_get_primary_monitor();
            if (checksum != 2303) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_get_primary_monitor` checksum `2303`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_get_screen_dimensions();
            if (checksum != 2379) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_get_screen_dimensions` checksum `2379`, library returned `{checksum}`");
            }
        }
        {
            var checksum = _UniFFILib.uniffi_rust_lib_checksum_func_get_working_area();
            if (checksum != 50016) {
                throw new UniffiContractChecksumException($"uniffi.rust_lib: uniffi bindings expected function `uniffi_rust_lib_checksum_func_get_working_area` checksum `50016`, library returned `{checksum}`");
            }
        }
    }
}

// Public interface members begin here.

#pragma warning disable 8625

class FfiConverterUInt32: FfiConverter<uint, uint> {
    public static FfiConverterUInt32 INSTANCE = new FfiConverterUInt32();

    public override uint Lift(uint value) {
        return value;
    }

    public override uint Read(BigEndianStream stream) {
        return stream.ReadUInt();
    }

    public override uint Lower(uint value) {
        return value;
    }

    public override int AllocationSize(uint value) {
        return 4;
    }

    public override void Write(uint value, BigEndianStream stream) {
        stream.WriteUInt(value);
    }
}

class FfiConverterInt32: FfiConverter<int, int> {
    public static FfiConverterInt32 INSTANCE = new FfiConverterInt32();

    public override int Lift(int value) {
        return value;
    }

    public override int Read(BigEndianStream stream) {
        return stream.ReadInt();
    }

    public override int Lower(int value) {
        return value;
    }

    public override int AllocationSize(int value) {
        return 4;
    }

    public override void Write(int value, BigEndianStream stream) {
        stream.WriteInt(value);
    }
}

class FfiConverterString: FfiConverter<string, RustBuffer> {
    public static FfiConverterString INSTANCE = new FfiConverterString();

    // Note: we don't inherit from FfiConverterRustBuffer, because we use a
    // special encoding when lowering/lifting.  We can use `RustBuffer.len` to
    // store our length and avoid writing it out to the buffer.
    public override string Lift(RustBuffer value) {
        try {
            var bytes = value.AsStream().ReadBytes(value.len);
            return System.Text.Encoding.UTF8.GetString(bytes);
        } finally {
            RustBuffer.Free(value);
        }
    }

    public override string Read(BigEndianStream stream) {
        var length = stream.ReadInt();
        var bytes = stream.ReadBytes(length);
        return System.Text.Encoding.UTF8.GetString(bytes);
    }

    public override RustBuffer Lower(string value) {
        var bytes = System.Text.Encoding.UTF8.GetBytes(value);
        var rbuf = RustBuffer.Alloc(bytes.Length);
        rbuf.AsWriteableStream().WriteBytes(bytes);
        return rbuf;
    }

    // TODO(CS)
    // We aren't sure exactly how many bytes our string will be once it's UTF-8
    // encoded.  Allocate 3 bytes per unicode codepoint which will always be
    // enough.
    public override int AllocationSize(string value) {
        const int sizeForLength = 4;
        var sizeForString = value.Length * 3;
        return sizeForLength + sizeForString;
    }

    public override void Write(string value, BigEndianStream stream) {
        var bytes = System.Text.Encoding.UTF8.GetBytes(value);
        stream.WriteInt(bytes.Length);
        stream.WriteBytes(bytes);
    }
}

class FfiConverterByteArray: FfiConverterRustBuffer<byte[]> {
    public static FfiConverterByteArray INSTANCE = new FfiConverterByteArray();

    public override byte[] Read(BigEndianStream stream) {
        var length = stream.ReadInt();
        return stream.ReadBytes(length);
    }

    public override int AllocationSize(byte[] value) {
        return 4 + value.Length;
    }

    public override void Write(byte[] value, BigEndianStream stream) {
        stream.WriteInt(value.Length);
        stream.WriteBytes(value);
    }
}

internal record ImageData (
    byte[]? @image,
    uint @width,
    uint @height
) {
}

class FfiConverterTypeImageData: FfiConverterRustBuffer<ImageData> {
    public static FfiConverterTypeImageData INSTANCE = new FfiConverterTypeImageData();

    public override ImageData Read(BigEndianStream stream) {
        return new ImageData(
            @image: FfiConverterOptionalByteArray.INSTANCE.Read(stream),
            @width: FfiConverterUInt32.INSTANCE.Read(stream),
            @height: FfiConverterUInt32.INSTANCE.Read(stream)
        );
    }

    public override int AllocationSize(ImageData value) {
        return 0
            + FfiConverterOptionalByteArray.INSTANCE.AllocationSize(value.@image)
            + FfiConverterUInt32.INSTANCE.AllocationSize(value.@width)
            + FfiConverterUInt32.INSTANCE.AllocationSize(value.@height);
    }

    public override void Write(ImageData value, BigEndianStream stream) {
            FfiConverterOptionalByteArray.INSTANCE.Write(value.@image, stream);
            FfiConverterUInt32.INSTANCE.Write(value.@width, stream);
            FfiConverterUInt32.INSTANCE.Write(value.@height, stream);
    }
}

internal record MonitorData (
    uint @width,
    uint @height,
    int @x,
    int @y,
    String @name
) {
}

class FfiConverterTypeMonitorData: FfiConverterRustBuffer<MonitorData> {
    public static FfiConverterTypeMonitorData INSTANCE = new FfiConverterTypeMonitorData();

    public override MonitorData Read(BigEndianStream stream) {
        return new MonitorData(
            @width: FfiConverterUInt32.INSTANCE.Read(stream),
            @height: FfiConverterUInt32.INSTANCE.Read(stream),
            @x: FfiConverterInt32.INSTANCE.Read(stream),
            @y: FfiConverterInt32.INSTANCE.Read(stream),
            @name: FfiConverterString.INSTANCE.Read(stream)
        );
    }

    public override int AllocationSize(MonitorData value) {
        return 0
            + FfiConverterUInt32.INSTANCE.AllocationSize(value.@width)
            + FfiConverterUInt32.INSTANCE.AllocationSize(value.@height)
            + FfiConverterInt32.INSTANCE.AllocationSize(value.@x)
            + FfiConverterInt32.INSTANCE.AllocationSize(value.@y)
            + FfiConverterString.INSTANCE.AllocationSize(value.@name);
    }

    public override void Write(MonitorData value, BigEndianStream stream) {
            FfiConverterUInt32.INSTANCE.Write(value.@width, stream);
            FfiConverterUInt32.INSTANCE.Write(value.@height, stream);
            FfiConverterInt32.INSTANCE.Write(value.@x, stream);
            FfiConverterInt32.INSTANCE.Write(value.@y, stream);
            FfiConverterString.INSTANCE.Write(value.@name, stream);
    }
}

internal class CaptureException: UniffiException {
    CaptureException(string message): base(message) {}

    // Each variant is a nested class
    // Flat enums carries a string error message, so no special implementation is necessary.

    public class Failed: CaptureException {
        public Failed(string message): base(message) {}
    }

}

class FfiConverterTypeCaptureException : FfiConverterRustBuffer<CaptureException>, CallStatusErrorHandler<CaptureException> {
    public static FfiConverterTypeCaptureException INSTANCE = new FfiConverterTypeCaptureException();

    public override CaptureException Read(BigEndianStream stream) {
        var value = stream.ReadInt();
        switch (value) {
            case 1: return new CaptureException.Failed(FfiConverterString.INSTANCE.Read(stream));
            default:
                throw new InternalException(String.Format("invalid error value '{0}' in FfiConverterTypeCaptureException.Read()", value));
        }
    }

    public override int AllocationSize(CaptureException value) {
        return 4 + FfiConverterString.INSTANCE.AllocationSize(value.Message);
    }

    public override void Write(CaptureException value, BigEndianStream stream) {
        switch (value) {
            case CaptureException.Failed:
                stream.WriteInt(1);
                break;
            default:
                throw new InternalException(String.Format("invalid error value '{0}' in FfiConverterTypeCaptureException.Write()", value));
        }
    }
}

class FfiConverterOptionalByteArray: FfiConverterRustBuffer<byte[]?> {
    public static FfiConverterOptionalByteArray INSTANCE = new FfiConverterOptionalByteArray();

    public override byte[]? Read(BigEndianStream stream) {
        if (stream.ReadByte() == 0) {
            return null;
        }
        return FfiConverterByteArray.INSTANCE.Read(stream);
    }

    public override int AllocationSize(byte[]? value) {
        if (value == null) {
            return 1;
        } else {
            return 1 + FfiConverterByteArray.INSTANCE.AllocationSize((byte[])value);
        }
    }

    public override void Write(byte[]? value, BigEndianStream stream) {
        if (value == null) {
            stream.WriteByte(0);
        } else {
            stream.WriteByte(1);
            FfiConverterByteArray.INSTANCE.Write((byte[])value, stream);
        }
    }
}
#pragma warning restore 8625
internal static class RustLibMethods {
    public static ImageData CaptureFullscreen() {
        return FfiConverterTypeImageData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_capture_fullscreen( ref _status)
));
    }

    public static ImageData CaptureMonitor(String @name) {
        return FfiConverterTypeImageData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_capture_monitor(FfiConverterString.INSTANCE.Lower(@name), ref _status)
));
    }

    public static ImageData CaptureRect(uint @x, uint @y, uint @width, uint @height) {
        return FfiConverterTypeImageData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_capture_rect(FfiConverterUInt32.INSTANCE.Lower(@x), FfiConverterUInt32.INSTANCE.Lower(@y), FfiConverterUInt32.INSTANCE.Lower(@width), FfiConverterUInt32.INSTANCE.Lower(@height), ref _status)
));
    }

    public static ImageData CaptureWindow(uint @x, uint @y) {
        return FfiConverterTypeImageData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_capture_window(FfiConverterUInt32.INSTANCE.Lower(@x), FfiConverterUInt32.INSTANCE.Lower(@y), ref _status)
));
    }

    /// <exception cref="CaptureException"></exception>
    public static ImageData CaptureWindowByTitle(String @name) {
        return FfiConverterTypeImageData.INSTANCE.Lift(
    _UniffiHelpers.RustCallWithError(FfiConverterTypeCaptureException.INSTANCE, (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_capture_window_by_title(FfiConverterString.INSTANCE.Lower(@name), ref _status)
));
    }

    public static MonitorData GetMonitor(uint @x, uint @y) {
        return FfiConverterTypeMonitorData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_get_monitor(FfiConverterUInt32.INSTANCE.Lower(@x), FfiConverterUInt32.INSTANCE.Lower(@y), ref _status)
));
    }

    public static MonitorData GetPrimaryMonitor() {
        return FfiConverterTypeMonitorData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_get_primary_monitor( ref _status)
));
    }

    public static MonitorData GetScreenDimensions(String @name) {
        return FfiConverterTypeMonitorData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_get_screen_dimensions(FfiConverterString.INSTANCE.Lower(@name), ref _status)
));
    }

    public static MonitorData GetWorkingArea() {
        return FfiConverterTypeMonitorData.INSTANCE.Lift(
    _UniffiHelpers.RustCall( (ref RustCallStatus _status) =>
    _UniFFILib.uniffi_rust_lib_fn_func_get_working_area( ref _status)
));
    }

}

using System.Runtime.CompilerServices;

using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

namespace PersonaEngine.Lib.LLM;

public class VisualQASemanticKernelChatEngine : IVisualChatEngine
{
    private readonly IChatCompletionService _chatCompletionService;

    private readonly ILogger<VisualQASemanticKernelChatEngine> _logger;

    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public VisualQASemanticKernelChatEngine(Kernel kernel, ILogger<VisualQASemanticKernelChatEngine> logger)
    {
        _logger                = logger ?? throw new ArgumentNullException(nameof(logger));
        _chatCompletionService = kernel.GetRequiredService<IChatCompletionService>("vision");
    }

    public void Dispose()
    {
        try
        {
            _logger.LogInformation("Disposing VisualQASemanticKernelChatEngine");
            _semaphore.Dispose();
            GC.SuppressFinalize(this);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while disposing VisualQASemanticKernelChatEngine");
        }
    }

    public async IAsyncEnumerable<string> GetStreamingChatResponseAsync(
        VisualChatMessage                          visualInput,
        PromptExecutionSettings?                   executionSettings = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await _semaphore.WaitAsync(cancellationToken);

        try
        {
            var chatHistory = new ChatHistory("You are a helpful assistant.");
            chatHistory.AddUserMessage(
            [
                new TextContent(visualInput.Query),
                new ImageContent(visualInput.ImageData, "image/png")
            ]);

            var chunkCount = 0;

            var streamingResponse = _chatCompletionService.GetStreamingChatMessageContentsAsync(
                                                                                chatHistory,
                                                                                executionSettings,
                                                                                null,
                                                                                cancellationToken);

            await foreach ( var chunk in streamingResponse.ConfigureAwait(false) )
            {
                cancellationToken.ThrowIfCancellationRequested();

                chunkCount++;
                var content = chunk.Content ?? string.Empty;

                yield return content;
            }

            _logger.LogInformation("Visual QA response streaming completed. Total chunks: {ChunkCount}", chunkCount);
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

using System.Text.RegularExpressions;

using Microsoft.Extensions.Logging;

using OpenNLP.Tools.PosTagger;
using OpenNLP.Tools.Tokenize;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Implementation of OpenNLP-based POS tagger with spaCy-like currency handling
/// </summary>
public partial class OpenNlpPosTagger : IPosTagger
{
    private readonly Regex _currencySymbolRegex = CurrencyRegex();

    private readonly ILogger<OpenNlpPosTagger> _logger;

    private readonly EnglishMaximumEntropyPosTagger _posTagger;

    private readonly EnglishRuleBasedTokenizer _tokenizer;

    private bool _disposed;

    public OpenNlpPosTagger(string modelPath, ILogger<OpenNlpPosTagger> logger)
    {
        if ( string.IsNullOrEmpty(modelPath) )
        {
            throw new ArgumentException("Model path cannot be null or empty", nameof(modelPath));
        }

        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        try
        {
            _tokenizer = new EnglishRuleBasedTokenizer(false);
            _posTagger = new EnglishMaximumEntropyPosTagger(modelPath);
            _logger.LogInformation("Initialized OpenNLP POS tagger from {ModelPath}", modelPath);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize OpenNLP POS tagger");

            throw;
        }
    }

    /// <summary>
    ///     Tags parts of speech in text using OpenNLP with spaCy-like currency handling
    /// </summary>
    public Task<IReadOnlyList<PosToken>> TagAsync(string text, CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return Task.FromResult<IReadOnlyList<PosToken>>(Array.Empty<PosToken>());
        }

        try
        {
            cancellationToken.ThrowIfCancellationRequested();

            // Pre-process text to insert spaces between currency symbols and digits
            // This makes the tokenizer split currency symbols from amounts, similar to spaCy
            var currencyRegex = CurrencyWithNumRegex();
            var processedText = currencyRegex.Replace(text, "$1 $2");

            // Tokenize pre-processed text
            var tokens = _tokenizer.Tokenize(processedText);

            // Get POS tags
            var tags = _posTagger.Tag(tokens);

            // Post-process to assign "$" tag to currency symbols
            for ( var i = 0; i < tokens.Length; i++ )
            {
                if ( _currencySymbolRegex.IsMatch(tokens[i]) )
                {
                    tags[i] = "$"; // Assign "$" tag to currency symbols (spaCy-like behavior)
                }
            }

            // Build result tokens with whitespace
            var result          = new List<PosToken>();
            var currentPosition = 0;

            for ( var i = 0; i < tokens.Length; i++ )
            {
                var token = tokens[i];
                var tag   = tags[i];

                // Find token position in processed text
                var tokenPosition = processedText.IndexOf(token, currentPosition, StringComparison.Ordinal);

                // Extract whitespace between tokens
                var whitespace = "";
                if ( i < tokens.Length - 1 )
                {
                    var nextTokenStart = processedText.IndexOf(tokens[i + 1], tokenPosition + token.Length, StringComparison.Ordinal);
                    if ( nextTokenStart >= 0 )
                    {
                        whitespace = processedText.Substring(
                                                             tokenPosition + token.Length,
                                                             nextTokenStart - (tokenPosition + token.Length));
                    }
                }
                else
                {
                    // Last token - get any remaining whitespace
                    whitespace = tokenPosition + token.Length < processedText.Length
                                     ? processedText[(tokenPosition + token.Length)..]
                                     : "";
                }

                result.Add(new PosToken { Text = token, PartOfSpeech = tag, IsWhitespace = whitespace.Contains(" ") });

                currentPosition = tokenPosition + token.Length;
            }

            return Task.FromResult<IReadOnlyList<PosToken>>(result);
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("POS tagging was canceled");

            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during POS tagging");

            throw;
        }
    }

    public void Dispose()
    {
        if ( _disposed )
        {
            return;
        }

        _disposed = true;
    }

    [GeneratedRegex(@"([\$\€\£\¥\₹])(\d)")]
    private static partial Regex CurrencyWithNumRegex();

    [GeneratedRegex(@"^[\$\€\£\¥\₹]$")] private static partial Regex CurrencyRegex();
}

using System.Text;

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
/// Processes raw AudioSegments into structured SubtitleSegments containing lines and words
/// with calculated timing and layout information.
/// </summary>
public class SubtitleProcessor(TextMeasurer textMeasurer, float defaultWordDuration = 0.3f)
{
    private readonly float _defaultWordDuration = Math.Max(0.01f, defaultWordDuration);

    public SubtitleSegment ProcessSegment(AudioSegment audioSegment, float segmentAbsoluteStartTime)
    {
        if ( audioSegment?.Tokens == null || audioSegment.Tokens.Count == 0 )
        {
            return new SubtitleSegment(audioSegment!, segmentAbsoluteStartTime, string.Empty);
        }

        // --- 1. Build Full Text ---

        var textBuilder = new StringBuilder();
        foreach ( var token in audioSegment.Tokens )
        {
            textBuilder.Append(token.Text);
            textBuilder.Append(token.Whitespace);
        }

        var fullText = textBuilder.ToString();

        var processedSegment     = new SubtitleSegment(audioSegment, segmentAbsoluteStartTime, fullText);
        var currentLine          = new SubtitleLine(0, 0);
        var currentLineWidth     = 0f;
        var cumulativeTimeOffset = 0f;

        // --- 2. Iterate Tokens, Calculate Timing & Layout ---
        for ( var i = 0; i < audioSegment.Tokens.Count; i++ )
        {
            var token    = audioSegment.Tokens[i];
            var wordText = token.Text + token.Whitespace;
            var wordSize = textMeasurer.MeasureText(wordText);

            // --- 3. Line Breaking ---
            if ( currentLineWidth > 0 && currentLineWidth + wordSize.X > textMeasurer.AvailableWidth )
            {
                processedSegment.AddLine(currentLine);
                currentLine      = new SubtitleLine(processedSegment.Lines.Count, 0); // TODO: Get segment index properly if needed
                currentLineWidth = 0f;
            }

            // --- 4. Word Timing Calculation ---
            float wordStartTimeOffset;
            float wordDuration;

            if ( token.StartTs.HasValue )
            {
                wordStartTimeOffset = (float)token.StartTs.Value;
                if ( token.EndTs.HasValue )
                {
                    // Case 1: Start and End provided
                    wordDuration = (float)(token.EndTs.Value - token.StartTs.Value);
                }
                else
                {
                    // Case 2: Only Start provided - Estimate duration until next token or use default
                    var nextTokenStartOffset = FindNextTokenStartOffset(audioSegment, i);
                    if ( nextTokenStartOffset > wordStartTimeOffset )
                    {
                        wordDuration = nextTokenStartOffset - wordStartTimeOffset;
                    }
                    else
                    {
                        wordDuration = _defaultWordDuration;
                    }
                }
            }
            else
            {
                // Case 3: No Start provided - Estimate start based on previous word's end or cumulative time
                wordStartTimeOffset = cumulativeTimeOffset;
                var nextTokenStartOffset = FindNextTokenStartOffset(audioSegment, i);
                if ( nextTokenStartOffset > wordStartTimeOffset )
                {
                    wordDuration = nextTokenStartOffset - wordStartTimeOffset;
                }
                else
                {
                    wordDuration = _defaultWordDuration;
                }
            }

            wordDuration = Math.Max(0.01f, wordDuration);

            cumulativeTimeOffset = wordStartTimeOffset + wordDuration;

            // --- 5. Create Word Info ---
            var wordInfo = new SubtitleWordInfo { Text = wordText, Size = wordSize, AbsoluteStartTime = segmentAbsoluteStartTime + wordStartTimeOffset, Duration = wordDuration };

            currentLine.AddWord(wordInfo);
            currentLineWidth += wordSize.X;
        }

        if ( currentLine.Words.Count > 0 )
        {
            processedSegment.AddLine(currentLine);
        }

        return processedSegment;
    }

    private float FindNextTokenStartOffset(AudioSegment segment, int currentTokenIndex)
    {
        for ( var j = currentTokenIndex + 1; j < segment.Tokens.Count; j++ )
        {
            if ( segment.Tokens[j].StartTs.HasValue )
            {
                return (float)segment.Tokens[j].StartTs!.Value;
            }
        }

        // Indicate not found or end of segment
        // Return a value that ensures the calling logic uses the default duration.
        // Returning -1 or float.MaxValue could work, depending on how it's used.
        // Let's return a value <= the current offset to trigger default duration.
        return segment.Tokens[currentTokenIndex].StartTs.HasValue ? (float)segment.Tokens[currentTokenIndex].StartTs!.Value : 0f;
    }
}

using System.Runtime.InteropServices;

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

public class CubismRenderer_OpenGLES2 : CubismRenderer
{
    public const int ColorChannelCount = 4; // 実験時に1チャンネルの場合は1、RGBだけの場合は3、アルファも含める場合は4

    public const int ClippingMaskMaxCountOnDefault = 36; // 通常のフレームバッファ1枚あたりのマスク最大数

    public const int ClippingMaskMaxCountOnMultiRenderTexture = 32; // フレームバッファが2枚以上ある場合のフレームバッファ1枚あたりのマスク最大数

    /// <summary>
    ///     マスク描画用のフレームバッファ
    /// </summary>
    private readonly List<CubismOffscreenSurface_OpenGLES2> _offscreenFrameBuffers = [];

    /// <summary>
    ///     OpenGLのステートを保持するオブジェクト
    /// </summary>
    private readonly CubismRendererProfile_OpenGLES2 _rendererProfile;

    /// <summary>
    ///     描画オブジェクトのインデックスを描画順に並べたリスト
    /// </summary>
    private readonly int[] _sortedDrawableIndexList;

    /// <summary>
    ///     モデルが参照するテクスチャとレンダラでバインドしているテクスチャとのマップ
    /// </summary>
    private readonly Dictionary<int, int> _textures;

    private readonly OpenGLApi GL;

    /// <summary>
    ///     クリッピングマスク管理オブジェクト
    /// </summary>
    private CubismClippingManager_OpenGLES2? _clippingManager;

    internal CubismShader_OpenGLES2 Shader;

    internal VBO[] vbo = new VBO[512];

    public CubismRenderer_OpenGLES2(OpenGLApi gl, CubismModel model, int maskBufferCount = 1) : base(model)
    {
        GL               = gl;
        Shader           = new CubismShader_OpenGLES2(gl);
        _rendererProfile = new CubismRendererProfile_OpenGLES2(gl);
        _textures        = new Dictionary<int, int>(32);

        VertexArray  = GL.GenVertexArray();
        VertexBuffer = GL.GenBuffer();
        IndexBuffer  = GL.GenBuffer();

        // 1未満は1に補正する
        if ( maskBufferCount < 1 )
        {
            maskBufferCount = 1;
            CubismLog.Warning("[Live2D SDK]The number of render textures must be an integer greater than or equal to 1. Set the number of render textures to 1.");
        }

        if ( model.IsUsingMasking() )
        {
            _clippingManager = new CubismClippingManager_OpenGLES2(GL); //クリッピングマスク・バッファ前処理方式を初期化
            _clippingManager.Initialize(model, maskBufferCount);

            _offscreenFrameBuffers.Clear();
            for ( var i = 0; i < maskBufferCount; ++i )
            {
                var offscreenSurface = new CubismOffscreenSurface_OpenGLES2(GL);
                offscreenSurface.CreateOffscreenSurface((int)_clippingManager.ClippingMaskBufferSize.X, (int)_clippingManager.ClippingMaskBufferSize.Y);
                _offscreenFrameBuffers.Add(offscreenSurface);
            }
        }

        _sortedDrawableIndexList = new int[model.GetDrawableCount()];
    }

    /// <summary>
    ///     マスクテクスチャに描画するためのクリッピングコンテキスト
    /// </summary>
    internal CubismClippingContext? ClippingContextBufferForMask { get; set; }

    /// <summary>
    ///     画面上描画するためのクリッピングコンテキスト
    /// </summary>
    internal CubismClippingContext? ClippingContextBufferForDraw { get; set; }

    internal int VertexArray { get; private set; }

    internal int VertexBuffer { get; private set; }

    internal int IndexBuffer { get; private set; }

    /// <summary>
    ///     Tegraプロセッサ対応。拡張方式による描画の有効・無効
    /// </summary>
    /// <param name="extMode">trueなら拡張方式で描画する</param>
    /// <param name="extPAMode">trueなら拡張方式のPA設定を有効にする</param>
    public void SetExtShaderMode(bool extMode, bool extPAMode = false)
    {
        Shader.SetExtShaderMode(extMode, extPAMode);
        Shader.ReleaseShaderProgram();
    }

    /// <summary>
    ///     OpenGLテクスチャのバインド処理
    ///     CubismRendererにテクスチャを設定し、CubismRenderer中でその画像を参照するためのIndex値を戻り値とする
    /// </summary>
    /// <param name="modelTextureNo">セットするモデルテクスチャの番号</param>
    /// <param name="glTextureNo">OpenGLテクスチャの番号</param>
    public void BindTexture(int modelTextureNo, int glTextureNo) { _textures[modelTextureNo] = glTextureNo; }

    /// <summary>
    ///     OpenGLにバインドされたテクスチャのリストを取得する
    /// </summary>
    /// <returns>テクスチャのアドレスのリスト</returns>
    public Dictionary<int, int> GetBindedTextures() { return _textures; }

    /// <summary>
    ///     クリッピングマスクバッファのサイズを設定する
    ///     マスク用のFrameBufferを破棄・再作成するため処理コストは高い。
    /// </summary>
    /// <param name="width">クリッピングマスクバッファのサイズ</param>
    /// <param name="height">クリッピングマスクバッファのサイズ</param>
    public void SetClippingMaskBufferSize(float width, float height)
    {
        if ( _clippingManager == null )
        {
            return;
        }

        // インスタンス破棄前にレンダーテクスチャの数を保存
        var renderTextureCount = _clippingManager.RenderTextureCount;

        _clippingManager = new CubismClippingManager_OpenGLES2(GL);

        _clippingManager.SetClippingMaskBufferSize(width, height);

        _clippingManager.Initialize(Model, renderTextureCount);
    }

    /// <summary>
    ///     クリッピングマスクのバッファを取得する
    /// </summary>
    /// <returns>クリッピングマスクのバッファへのポインタ</returns>
    public CubismOffscreenSurface_OpenGLES2 GetMaskBuffer(int index) { return _offscreenFrameBuffers[index]; }

    internal override unsafe void DrawMesh(int     textureNo,  int             indexCount,     int    vertexCount
                                         , ushort* indexArray, float*          vertexArray,    float* uvArray
                                         , float   opacity,    CubismBlendMode colorBlendMode, bool   invertedMask)
    {
        throw new Exception("[Live2D Core]Use 'DrawMeshOpenGL' function");
    }

    public bool IsGeneratingMask() { return ClippingContextBufferForMask != null; }

    /// <summary>
    ///     [オーバーライド]
    ///     描画オブジェクト（アートメッシュ）を描画する。
    ///     ポリゴンメッシュとテクスチャ番号をセットで渡す。
    /// </summary>
    /// <param name="textureNo">描画するテクスチャ番号</param>
    /// <param name="indexCount">描画オブジェクトのインデックス値</param>
    /// <param name="vertexCount">ポリゴンメッシュの頂点数</param>
    /// <param name="indexArray">ポリゴンメッシュのインデックス配列</param>
    /// <param name="vertexArray">ポリゴンメッシュの頂点配列</param>
    /// <param name="uvArray">uv配列</param>
    /// <param name="multiplyColor">乗算色</param>
    /// <param name="screenColor">スクリーン色</param>
    /// <param name="opacity">不透明度</param>
    /// <param name="colorBlendMode">カラー合成タイプ</param>
    /// <param name="invertedMask">マスク使用時のマスクの反転使用</param>
    internal unsafe void DrawMeshOpenGL(CubismModel model, int index)
    {
        if ( _textures[model.GetDrawableTextureIndex(index)] == 0 )
        {
            return; // モデルが参照するテクスチャがバインドされていない場合は描画をスキップする
        }

        // 裏面描画の有効・無効
        if ( IsCulling )
        {
            GL.Enable(GL.GL_CULL_FACE);
        }
        else
        {
            GL.Disable(GL.GL_CULL_FACE);
        }

        GL.FrontFace(GL.GL_CCW); // Cubism SDK OpenGLはマスク・アートメッシュ共にCCWが表面

        var vertexCount = model.GetDrawableVertexCount(index);
        var vertexArray = model.GetDrawableVertices(index);
        var uvArray     = (float*)model.GetDrawableVertexUvs(index);

        GL.BindVertexArray(VertexArray);

        if ( vbo == null || vbo.Length < vertexCount )
        {
            vbo = new VBO[vertexCount];
        }

        for ( var a = 0; a < vertexCount; a++ )
        {
            vbo[a].ver0 = vertexArray[a * 2];
            vbo[a].ver1 = vertexArray[a * 2 + 1];
            vbo[a].uv0  = uvArray[a * 2];
            vbo[a].uv1  = uvArray[a * 2 + 1];
        }

        var indexCount = model.GetDrawableVertexIndexCount(index);
        var indexArray = model.GetDrawableVertexIndices(index);

        GL.BindBuffer(GL.GL_ARRAY_BUFFER, VertexBuffer);
        fixed (void* p = vbo)
        {
            GL.BufferData(GL.GL_ARRAY_BUFFER, vertexCount * sizeof(VBO), new IntPtr(p), GL.GL_STATIC_DRAW);
        }

        GL.BindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, IndexBuffer);
        GL.BufferData(GL.GL_ELEMENT_ARRAY_BUFFER, indexCount * sizeof(ushort), new IntPtr(indexArray), GL.GL_STATIC_DRAW);

        if ( IsGeneratingMask() ) // マスク生成時
        {
            Shader.SetupShaderProgramForMask(this, model, index);
        }
        else
        {
            Shader.SetupShaderProgramForDraw(this, model, index);
        }

        // ポリゴンメッシュを描画する
        GL.DrawElements(GL.GL_TRIANGLES, indexCount, GL.GL_UNSIGNED_SHORT, 0);

        // 後処理
        GL.UseProgram(0);
        GL.BindBuffer(GL.GL_ARRAY_BUFFER, 0);
        GL.BindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0);
        GL.BindVertexArray(0);
        ClippingContextBufferForDraw = null;
        ClippingContextBufferForMask = null;
    }

    /// <summary>
    ///     描画開始時の追加処理。
    ///     モデルを描画する前にクリッピングマスクに必要な処理を実装している。
    /// </summary>
    internal void PreDraw()
    {
        GL.Disable(GL.GL_SCISSOR_TEST);
        GL.Disable(GL.GL_STENCIL_TEST);
        GL.Disable(GL.GL_DEPTH_TEST);

        GL.Enable(GL.GL_BLEND);
        GL.ColorMask(true, true, true, true);

        if ( GL.IsPhoneES2 )
        {
            GL.BindVertexArrayOES(0);
        }

        GL.BindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0);
        GL.BindBuffer(GL.GL_ARRAY_BUFFER, 0); //前にバッファがバインドされていたら破棄する必要がある

        //異方性フィルタリング。プラットフォームのOpenGLによっては未対応の場合があるので、未設定のときは設定しない
        if ( Anisotropy > 0.0f )
        {
            for ( var i = 0; i < _textures.Count; i++ )
            {
                GL.BindTexture(GL.GL_TEXTURE_2D, _textures[i]);
                GL.TexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAX_ANISOTROPY_EXT, Anisotropy);
            }
        }
    }

    /// <summary>
    ///     モデル描画直前のOpenGLES2のステートを保持する
    /// </summary>
    protected override void SaveProfile() { _rendererProfile.Save(); }

    /// <summary>
    ///     モデル描画直前のOpenGLES2のステートを保持する
    /// </summary>
    protected override void RestoreProfile() { _rendererProfile.Restore(); }

    protected override unsafe void DoDrawModel()
    {
        //------------ クリッピングマスク・バッファ前処理方式の場合 ------------
        if ( _clippingManager != null )
        {
            PreDraw();

            // サイズが違う場合はここで作成しなおし
            for ( var i = 0; i < _clippingManager.RenderTextureCount; ++i )
            {
                if ( _offscreenFrameBuffers[i].BufferWidth != (uint)_clippingManager.ClippingMaskBufferSize.X ||
                     _offscreenFrameBuffers[i].BufferHeight != (uint)_clippingManager.ClippingMaskBufferSize.Y )
                {
                    _offscreenFrameBuffers[i].CreateOffscreenSurface(
                                                                     (int)_clippingManager.ClippingMaskBufferSize.X, (int)_clippingManager.ClippingMaskBufferSize.Y);
                }
            }

            if ( UseHighPrecisionMask )
            {
                _clippingManager.SetupMatrixForHighPrecision(Model, false);
            }
            else
            {
                _clippingManager.SetupClippingContext(Model, this, _rendererProfile.LastFBO, _rendererProfile.LastViewport);
            }
        }

        // 上記クリッピング処理内でも一度PreDrawを呼ぶので注意!!
        PreDraw();

        var drawableCount = Model.GetDrawableCount();
        var renderOrder   = Model.GetDrawableRenderOrders();

        // インデックスを描画順でソート
        for ( var i = 0; i < drawableCount; ++i )
        {
            var order = renderOrder[i];
            _sortedDrawableIndexList[order] = i;
        }

        if ( GL.AlwaysClear )
        {
            GL.ClearColor(ClearColor.R,
                          ClearColor.G,
                          ClearColor.B,
                          ClearColor.A);

            GL.Clear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
        }

        // 描画
        for ( var i = 0; i < drawableCount; ++i )
        {
            var drawableIndex = _sortedDrawableIndexList[i];

            // Drawableが表示状態でなければ処理をパスする
            if ( !Model.GetDrawableDynamicFlagIsVisible(drawableIndex) )
            {
                continue;
            }

            // クリッピングマスク
            var clipContext = _clippingManager?.ClippingContextListForDraw[drawableIndex];

            if ( clipContext != null && UseHighPrecisionMask ) // マスクを書く必要がある
            {
                if ( clipContext.IsUsing ) // 書くことになっていた
                {
                    // 生成したFrameBufferと同じサイズでビューポートを設定
                    GL.Viewport(0, 0, (int)_clippingManager!.ClippingMaskBufferSize.X, (int)_clippingManager.ClippingMaskBufferSize.Y);

                    PreDraw(); // バッファをクリアする

                    // ---------- マスク描画処理 ----------
                    // マスク用RenderTextureをactiveにセット
                    GetMaskBuffer(clipContext.BufferIndex).BeginDraw(_rendererProfile.LastFBO);

                    // マスクをクリアする
                    // 1が無効（描かれない）領域、0が有効（描かれる）領域。（シェーダで Cd*Csで0に近い値をかけてマスクを作る。1をかけると何も起こらない）
                    GL.ClearColor(1.0f, 1.0f, 1.0f, 1.0f);
                    GL.Clear(GL.GL_COLOR_BUFFER_BIT);
                }

                {
                    var clipDrawCount = clipContext.ClippingIdCount;
                    for ( var index = 0; index < clipDrawCount; index++ )
                    {
                        var clipDrawIndex = clipContext.ClippingIdList[index];

                        // 頂点情報が更新されておらず、信頼性がない場合は描画をパスする
                        if ( !Model.GetDrawableDynamicFlagVertexPositionsDidChange(clipDrawIndex) )
                        {
                            continue;
                        }

                        IsCulling = Model.GetDrawableCulling(clipDrawIndex);

                        // 今回専用の変換を適用して描く
                        // チャンネルも切り替える必要がある(A,R,G,B)
                        ClippingContextBufferForMask = clipContext;

                        DrawMeshOpenGL(Model, clipDrawIndex);
                    }
                }

                {
                    // --- 後処理 ---
                    GetMaskBuffer(clipContext.BufferIndex).EndDraw();
                    ClippingContextBufferForMask = null;
                    GL.Viewport(_rendererProfile.LastViewport[0], _rendererProfile.LastViewport[1], _rendererProfile.LastViewport[2], _rendererProfile.LastViewport[3]);

                    PreDraw(); // バッファをクリアする
                }
            }

            // クリッピングマスクをセットする
            ClippingContextBufferForDraw = clipContext;

            IsCulling = Model.GetDrawableCulling(drawableIndex);

            DrawMeshOpenGL(Model, drawableIndex);
        }
    }

    public override void Dispose() { Shader.ReleaseShaderProgram(); }

    public int GetBindedTextureId(int textureId) { return _textures[textureId] != 0 ? _textures[textureId] : -1; }

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    internal struct VBO
    {
        public float ver0;

        public float ver1;

        public float uv0;

        public float uv1;
    }
}

using System.Collections.Concurrent;

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Utils;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     File-based model provider implementation
/// </summary>
public class FileModelProvider : IModelProvider
{
    private readonly string _baseDirectory;

    private readonly ILogger<FileModelProvider> _logger;

    private readonly ConcurrentDictionary<ModelType, ModelResource> _modelCache = new();

    private bool _disposed;

    public FileModelProvider(string baseDirectory, ILogger<FileModelProvider> logger)
    {
        _baseDirectory = baseDirectory ?? throw new ArgumentNullException(nameof(baseDirectory));
        _logger        = logger ?? throw new ArgumentNullException(nameof(logger));

        if ( !Directory.Exists(_baseDirectory) )
        {
            throw new DirectoryNotFoundException($"Model directory not found: {_baseDirectory}");
        }
    }

    /// <summary>
    ///     Gets a model by type
    /// </summary>
    public Task<ModelResource> GetModelAsync(ModelType modelType, CancellationToken cancellationToken = default)
    {
        // Check if already cached
        if ( _modelCache.TryGetValue(modelType, out var cachedModel) )
        {
            return Task.FromResult(cachedModel);
        }

        try
        {
            cancellationToken.ThrowIfCancellationRequested();

            // Map model type to path
            var modelPath = GetModelPath(modelType);

            // Create new model resource
            var model = new ModelResource(modelPath);

            // Cache the model
            _modelCache[modelType] = model;

            _logger.LogInformation("Loaded model {ModelType} from {Path}", modelType, modelPath);

            return Task.FromResult(model);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error loading model {ModelType}", modelType);

            throw;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if ( _disposed )
        {
            return;
        }

        // Dispose all cached models
        foreach ( var model in _modelCache.Values )
        {
            model.Dispose();
        }

        _modelCache.Clear();
        _disposed = true;

        await Task.CompletedTask;
    }

    /// <summary>
    ///     Maps model type to file path
    /// </summary>
    private string GetModelPath(ModelType modelType)
    {
        var fullPath = Path.Combine(_baseDirectory, modelType.GetDescription());

        return fullPath;
    }
}

using System.Numerics;

using FontStashSharp;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Manages active subtitle segments, updates their state based on time,
///     determines visible lines, and calculates their positions.
/// </summary>
public class SubtitleTimeline
{
    private readonly List<SubtitleSegment> _activeSegments = new();

    private readonly float _bottomMargin;

    private readonly FSColor _highlightColor;

    private readonly float _interSegmentSpacing;

    private readonly float _lineSpacing;

    private readonly Lock _lock = new();

    private readonly int _maxVisibleLines;

    private readonly FSColor _normalColor;

    private readonly TextMeasurer _textMeasurer;

    private readonly List<SubtitleLine> _visibleLinesCache;

    private readonly IWordAnimator _wordAnimator;

    public SubtitleTimeline(
        int           maxVisibleLines,
        float         bottomMargin,
        float         lineSpacing,
        float         interSegmentSpacing,
        TextMeasurer  textMeasurer,
        IWordAnimator wordAnimator,
        FSColor       highlightColor,
        FSColor       normalColor)
    {
        _maxVisibleLines     = Math.Max(1, maxVisibleLines);
        _bottomMargin        = bottomMargin;
        _lineSpacing         = lineSpacing;
        _interSegmentSpacing = interSegmentSpacing;
        _textMeasurer        = textMeasurer;
        _wordAnimator        = wordAnimator;
        _highlightColor      = highlightColor;
        _normalColor         = normalColor;

        _visibleLinesCache = new List<SubtitleLine>(_maxVisibleLines * 2);
    }

    public void AddSegment(SubtitleSegment segment)
    {
        lock (_lock)
        {
            _activeSegments.Add(segment);
        }
    }

    public void RemoveSegment(object originalSegmentIdentifier)
    {
        lock (_lock)
        {
            _activeSegments.RemoveAll(s => ReferenceEquals(s.OriginalAudioSegment, originalSegmentIdentifier) || s.OriginalAudioSegment.Equals(originalSegmentIdentifier));

            // TODO: If using object pooling for segments/lines/words, return them to the pool here.
        }
    }

    /// <summary>
    ///     Updates the animation progress and state of words in active segments.
    /// </summary>
    public void Update(float currentTime)
    {
        lock (_lock)
        {
            for ( var i = _activeSegments.Count - 1; i >= 0; i-- )
            {
                var segment = _activeSegments[i];

                // Optimization: Potentially skip segments that are entirely in the past?
                // if (segment.EstimatedEndTime < currentTime - someBuffer) continue;
                // Optimization: Potentially skip segments entirely in the future?
                // if (segment.AbsoluteStartTime > currentTime + someBuffer) continue;

                for ( var j = 0; j < segment.Lines.Count; j++ )
                {
                    var line = segment.Lines[j];
                    // Use ref local for structs to avoid copying if SubtitleWordInfo is a struct
                    // Requires Words to be an array or Span<T> for direct ref access.
                    // With List<T>, we modify the copy and then update the list element.
                    for ( var k = 0; k < line.Words.Count; k++ )
                    {
                        var word = line.Words[k];

                        if ( word.HasStarted(currentTime) )
                        {
                            if ( word.IsComplete(currentTime) )
                            {
                                word.AnimationProgress = 1.0f;
                            }
                            else
                            {
                                var elapsed = currentTime - word.AbsoluteStartTime;
                                word.AnimationProgress = Math.Clamp(elapsed / word.Duration, 0.0f, 1.0f);
                            }
                        }
                        else
                        {
                            word.AnimationProgress = 0.0f;
                        }

                        word.CurrentScale = _wordAnimator.CalculateScale(word.AnimationProgress);
                        word.CurrentColor = _wordAnimator.CalculateColor(_highlightColor, _normalColor, word.AnimationProgress);

                        line.Words[k] = word;
                    }
                }
            }
        }
    }

    public List<SubtitleLine> GetVisibleLinesAndPosition(float currentTime, int viewportWidth, int viewportHeight)
    {
        _visibleLinesCache.Clear();

        lock (_lock)
        {
            for ( var i = _activeSegments.Count - 1; i >= 0; i-- )
            {
                var segment = _activeSegments[i];

                // Optimization: If segment hasn't started, none of its lines are visible.
                if ( segment.AbsoluteStartTime > currentTime )
                {
                    continue;
                }

                for ( var j = segment.Lines.Count - 1; j >= 0; j-- )
                {
                    var line = segment.Lines[j];

                    if ( line.Words.Count > 0 && line.Words[0].HasStarted(currentTime) )
                    {
                        _visibleLinesCache.Add(line);
                        if ( _visibleLinesCache.Count >= _maxVisibleLines )
                        {
                            goto FoundEnoughLines;
                        }
                    }

                    // Optimization: If the first word of this line hasn't started,
                    // earlier lines in the *same segment* also won't have started yet.
                    // (Assumes words within a line are chronologically ordered).
                    // else if (line.Words.Count > 0)
                    // {
                    //     break; // Stop checking lines in this segment
                    // }
                }
            }
        }

        FoundEnoughLines:

        _visibleLinesCache.Reverse();

        var currentBaselineY = viewportHeight - _bottomMargin;

        for ( var i = _visibleLinesCache.Count - 1; i >= 0; i-- )
        {
            var line = _visibleLinesCache[i];
            line.BaselineY = currentBaselineY;

            var currentX = (viewportWidth - line.TotalWidth) / 2.0f;

            for ( var k = 0; k < line.Words.Count; k++ )
            {
                var word = line.Words[k];

                var wordCenterX = currentX + word.Size.X / 2.0f;
                var wordCenterY = currentBaselineY - _textMeasurer.LineHeight / 2.0f;

                word.Position =  new Vector2(wordCenterX, wordCenterY);
                line.Words[k] =  word;
                currentX      += word.Size.X;
            }

            currentBaselineY -= _lineSpacing;

            if ( i > 0 && _visibleLinesCache[i - 1].SegmentIndex != line.SegmentIndex )
            {
                currentBaselineY -= _interSegmentSpacing;
            }
        }

        return _visibleLinesCache;
    }
}

using System.Buffers.Binary;

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Serializer for converting float samples to and from PCM byte buffers.
/// </summary>
/// <remarks>
///     For now, this class only supports 8, 16, 24, 32 and 64 bits per sample.
/// </remarks>
public static class SampleSerializer
{
    /// <summary>
    ///     Deserialize the PCM byte buffer into a new float samples.
    /// </summary>
    /// <returns></returns>
    public static Memory<float> Deserialize(ReadOnlyMemory<byte> buffer, ushort bitsPerSample)
    {
        var floatBuffer = new float[buffer.Length / (bitsPerSample / 8)];
        Deserialize(buffer, floatBuffer, bitsPerSample);

        return floatBuffer.AsMemory();
    }

    /// <summary>
    ///     Serializes the float samples into a PCM byte buffer.
    /// </summary>
    /// <returns></returns>
    public static Memory<byte> Serialize(ReadOnlyMemory<float> samples, ushort bitsPerSample)
    {
        // Transform to long as we might overflow the int because of the multiplications (even if we cast to int later)
        var memoryBufferLength = (long)samples.Length * bitsPerSample / 8;

        var buffer = new byte[(int)memoryBufferLength];
        Serialize(samples, buffer, bitsPerSample);

        return buffer.AsMemory();
    }

    /// <summary>
    ///     Serializes the float samples into a PCM byte buffer.
    /// </summary>
    public static void Serialize(ReadOnlyMemory<float> samples, Memory<byte> buffer, ushort bitsPerSample)
    {
        var bytesPerSample = bitsPerSample / 8;
        var totalSamples   = samples.Length;
        var totalBytes     = totalSamples * bytesPerSample;

        if ( buffer.Length < totalBytes )
        {
            throw new ArgumentException("Buffer too small to hold the serialized data.");
        }

        var samplesSpan = samples.Span;
        var bufferSpan  = buffer.Span;

        var sampleIndex = 0;
        var bufferIndex = 0;

        while ( sampleIndex < totalSamples )
        {
            var sampleValue = samplesSpan[sampleIndex];
            WriteSample(bufferSpan, bufferIndex, sampleValue, bitsPerSample);
            bufferIndex += bytesPerSample;
            sampleIndex++;
        }
    }

    /// <summary>
    ///     Deserializes the PCM byte buffer into float samples.
    /// </summary>
    public static void Deserialize(ReadOnlyMemory<byte> buffer, Memory<float> samples, ushort bitsPerSample)
    {
        var bytesPerSample = bitsPerSample / 8;
        var totalSamples   = buffer.Length / bytesPerSample;

        if ( samples.Length < totalSamples )
        {
            throw new ArgumentException("Samples buffer is too small to hold the deserialized data.");
        }

        var bufferSpan  = buffer.Span;
        var samplesSpan = samples.Span;

        var sampleIndex = 0;
        var bufferIndex = 0;

        while ( bufferIndex < bufferSpan.Length )
        {
            var sampleValue = ReadSample(bufferSpan, ref bufferIndex, bitsPerSample);
            samplesSpan[sampleIndex++] = sampleValue;
            // bufferIndex is already incremented inside ReadSample
        }
    }

    /// <summary>
    ///     Reads a single sample from the byte span, considering the bit index and sample bit depth.
    /// </summary>
    internal static float ReadSample(ReadOnlySpan<byte> span, ref int index, ushort bitsPerSample)
    {
        var bytesPerSample = bitsPerSample / 8;

        float sampleValue;

        switch ( bitsPerSample )
        {
            case 8:
                var sampleByte = span[index];
                sampleValue = sampleByte / 127.5f - 1.0f;

                break;

            case 16:
                var sampleShort = BinaryPrimitives.ReadInt16LittleEndian(span.Slice(index, 2));
                sampleValue = sampleShort / 32768f;

                break;

            case 24:
                var sample24Bit = ReadInt24LittleEndian(span.Slice(index, 3));
                sampleValue = sample24Bit / 8388608f;

                break;

            case 32:
                var sampleInt = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(index, 4));
                sampleValue = sampleInt / 2147483648f;

                break;

            case 64:
                var sampleLong = BinaryPrimitives.ReadInt64LittleEndian(span.Slice(index, 8));
                sampleValue = sampleLong / 9223372036854775808f;

                break;

            default:
                throw new NotSupportedException($"Bits per sample {bitsPerSample} is not supported.");
        }

        index += bytesPerSample;

        return sampleValue;
    }

    /// <summary>
    ///     Writes a single sample into the byte span at the specified index.
    /// </summary>
    internal static void WriteSample(Span<byte> span, int index, float sampleValue, ushort bitsPerSample)
    {
        switch ( bitsPerSample )
        {
            case 8:
                var sampleByte = (byte)((sampleValue + 1.0f) * 127.5f);
                span[index] = sampleByte;

                break;

            case 16:
                var sampleShort = (short)(sampleValue * 32767f);
                BinaryPrimitives.WriteInt16LittleEndian(span.Slice(index, 2), sampleShort);

                break;

            case 24:
                var sample24Bit = (int)(sampleValue * 8388607f);
                WriteInt24LittleEndian(span.Slice(index, 3), sample24Bit);

                break;

            case 32:
                var sampleInt = (int)(sampleValue * 2147483647f);
                BinaryPrimitives.WriteInt32LittleEndian(span.Slice(index, 4), sampleInt);

                break;

            case 64:
                var sampleLong = (long)(sampleValue * 9223372036854775807f);
                BinaryPrimitives.WriteInt64LittleEndian(span.Slice(index, 8), sampleLong);

                break;

            default:
                throw new NotSupportedException($"Bits per sample {bitsPerSample} is not supported.");
        }
    }

    /// <summary>
    ///     Reads a 24-bit integer from a byte span in little-endian order.
    /// </summary>
    private static int ReadInt24LittleEndian(ReadOnlySpan<byte> span)
    {
        int b0     = span[0];
        int b1     = span[1];
        int b2     = span[2];
        var sample = (b2 << 16) | (b1 << 8) | b0;

        // Sign-extend to 32 bits if necessary
        if ( (sample & 0x800000) != 0 )
        {
            sample |= unchecked((int)0xFF000000);
        }

        return sample;
    }

    /// <summary>
    ///     Writes a 24-bit integer to a byte span in little-endian order.
    /// </summary>
    private static void WriteInt24LittleEndian(Span<byte> span, int value)
    {
        span[0] = (byte)(value & 0xFF);
        span[1] = (byte)((value >> 8) & 0xFF);
        span[2] = (byte)((value >> 16) & 0xFF);
    }
}

namespace PersonaEngine.Lib.Audio;

public readonly struct SliceAudioSource : IAudioSource
{
    private readonly long frameStart;

    private readonly int maxSliceFrames;

    private readonly IAudioSource audioSource;

    private readonly TimeSpan startTime;

    private readonly TimeSpan maxDuration;

    public uint SampleRate { get; }

    /// <summary>
    ///     Gets the total number of frames in the stream
    /// </summary>
    /// <remarks>
    ///     This can be more than actual number of frames in the source if the source is not big enough.
    /// </remarks>
    public long FramesCount => (long)(Duration.TotalMilliseconds * SampleRate / 1000);

    public ushort ChannelCount { get; }

    public bool IsInitialized => true;

    public ushort BitsPerSample { get; }

    public IReadOnlyDictionary<string, string> Metadata { get; }

    public TimeSpan Duration
    {
        get
        {
            var maxSourceDuration = audioSource.TotalDuration - startTime;

            return maxSourceDuration > maxDuration ? maxDuration : maxSourceDuration;
        }
    }

    public TimeSpan TotalDuration => Duration;

    /// <summary>
    ///     Creates a slice of an audio source (without copying the audio data).
    /// </summary>
    public SliceAudioSource(IAudioSource audioSource, TimeSpan startTime, TimeSpan maxDuration)
    {
        frameStart     = (long)(startTime.TotalMilliseconds * audioSource.SampleRate / 1000);
        maxSliceFrames = (int)(maxDuration.TotalMilliseconds * audioSource.SampleRate / 1000);
        SampleRate     = audioSource.SampleRate;

        if ( startTime >= audioSource.Duration )
        {
            throw new ArgumentOutOfRangeException(nameof(startTime), $"The start time is beyond the end of the audio source. Start time: {startTime}, Source Duration: {audioSource.Duration}");
        }

        ChannelCount     = audioSource.ChannelCount;
        BitsPerSample    = audioSource.BitsPerSample;
        this.audioSource = audioSource;
        this.maxDuration = maxDuration;
        Metadata         = audioSource.Metadata;
        this.startTime   = startTime;
    }

    public void Dispose() { }

    public Task<Memory<byte>> GetFramesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var adjustedMax = (int)Math.Min(maxFrames, maxSliceFrames - startFrame);

        return audioSource.GetFramesAsync(startFrame + frameStart, adjustedMax, cancellationToken);
    }

    public Task<Memory<float>> GetSamplesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var adjustedMax = (int)Math.Min(maxFrames, maxSliceFrames - startFrame);

        return audioSource.GetSamplesAsync(startFrame + frameStart, adjustedMax, cancellationToken);
    }

    public Task<int> CopyFramesAsync(Memory<byte> destination, long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        var adjustedMax = (int)Math.Min(maxFrames, maxSliceFrames - startFrame);

        return audioSource.CopyFramesAsync(destination, startFrame + frameStart, adjustedMax, cancellationToken);
    }
}

using System.Text.Json.Nodes;

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Effect;

/// <summary>
///     パーツの不透明度の管理と設定を行う。
/// </summary>
public class CubismPose
{
    public const float Epsilon = 0.001f;

    public const float DefaultFadeInSeconds = 0.5f;

    // Pose.jsonのタグ
    public const string FadeIn = "FadeInTime";

    public const string Link = "Link";

    public const string Groups = "Groups";

    public const string Id = "Id";

    /// <summary>
    ///     フェード時間[秒]
    /// </summary>
    private readonly float _fadeTimeSeconds = DefaultFadeInSeconds;

    /// <summary>
    ///     それぞれのパーツグループの個数
    /// </summary>
    private readonly List<int> _partGroupCounts = [];

    /// <summary>
    ///     パーツグループ
    /// </summary>
    private readonly List<PartData> _partGroups = [];

    /// <summary>
    ///     前回操作したモデル
    /// </summary>
    private CubismModel? _lastModel;

    /// <summary>
    ///     インスタンスを作成する。
    /// </summary>
    /// <param name="pose3json">pose3.jsonのデータ</param>
    public CubismPose(string pose3json)
    {
        using var stream = File.Open(pose3json, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        var json = JsonNode.Parse(stream)?.AsObject()
                   ?? throw new Exception("Pose json is error");

        // フェード時間の指定
        if ( json.ContainsKey(FadeIn) )
        {
            var item = json[FadeIn];
            _fadeTimeSeconds = item == null ? DefaultFadeInSeconds : (float)item;

            if ( _fadeTimeSeconds < 0.0f )
            {
                _fadeTimeSeconds = DefaultFadeInSeconds;
            }
        }

        // パーツグループ
        if ( json[Groups] is not JsonArray poseListInfo )
        {
            return;
        }

        foreach ( var item in poseListInfo )
        {
            var idCount    = item!.AsArray().Count;
            var groupCount = 0;

            for ( var groupIndex = 0; groupIndex < idCount; ++groupIndex )
            {
                var      partInfo = item[groupIndex]!;
                PartData partData = new() { PartId = CubismFramework.CubismIdManager.GetId(partInfo[Id]!.ToString()) };

                // リンクするパーツの設定
                if ( partInfo[Link] != null )
                {
                    var linkListInfo = partInfo[Link]!;
                    var linkCount    = linkListInfo.AsArray().Count;

                    for ( var linkIndex = 0; linkIndex < linkCount; ++linkIndex )
                    {
                        partData.Link.Add(new PartData { PartId = CubismFramework.CubismIdManager.GetId(linkListInfo[linkIndex]!.ToString()) });
                    }
                }

                _partGroups.Add(partData);

                ++groupCount;
            }

            _partGroupCounts.Add(groupCount);
        }
    }

    /// <summary>
    ///     モデルのパラメータを更新する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="deltaTimeSeconds">デルタ時間[秒]</param>
    public void UpdateParameters(CubismModel model, float deltaTimeSeconds)
    {
        // 前回のモデルと同じではないときは初期化が必要
        if ( model.Model != _lastModel?.Model )
        {
            // パラメータインデックスの初期化
            Reset(model);
        }

        _lastModel = model;

        // 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間0として対応。
        if ( deltaTimeSeconds < 0.0f )
        {
            deltaTimeSeconds = 0.0f;
        }

        var beginIndex = 0;

        foreach ( var item in _partGroupCounts )
        {
            DoFade(model, deltaTimeSeconds, beginIndex, item);

            beginIndex += item;
        }

        CopyPartOpacities(model);
    }

    /// <summary>
    ///     表示を初期化する。
    ///     不透明度の初期値が0でないパラメータは、不透明度を1に設定する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    public void Reset(CubismModel model)
    {
        var beginIndex = 0;

        foreach ( var item in _partGroupCounts )
        {
            for ( var j = beginIndex; j < beginIndex + item; ++j )
            {
                _partGroups[j].Initialize(model);

                var partsIndex = _partGroups[j].PartIndex;
                var paramIndex = _partGroups[j].ParameterIndex;

                if ( partsIndex < 0 )
                {
                    continue;
                }

                model.SetPartOpacity(partsIndex, j == beginIndex ? 1.0f : 0.0f);
                model.SetParameterValue(paramIndex, j == beginIndex ? 1.0f : 0.0f);

                for ( var k = 0; k < _partGroups[j].Link.Count; ++k )
                {
                    _partGroups[j].Link[k].Initialize(model);
                }
            }

            beginIndex += item;
        }
    }

    /// <summary>
    ///     パーツの不透明度をコピーし、リンクしているパーツへ設定する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    private void CopyPartOpacities(CubismModel model)
    {
        foreach ( var item in _partGroups )
        {
            if ( item.Link.Count == 0 )
            {
                continue; // 連動するパラメータはない
            }

            var partIndex = item.PartIndex;
            var opacity   = model.GetPartOpacity(partIndex);

            foreach ( var item1 in item.Link )
            {
                var linkPartIndex = item1.PartIndex;

                if ( linkPartIndex < 0 )
                {
                    continue;
                }

                model.SetPartOpacity(linkPartIndex, opacity);
            }
        }
    }

    /// <summary>
    ///     パーツのフェード操作を行う。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="deltaTimeSeconds">デルタ時間[秒]</param>
    /// <param name="beginIndex">フェード操作を行うパーツグループの先頭インデックス</param>
    /// <param name="partGroupCount">フェード操作を行うパーツグループの個数</param>
    private void DoFade(CubismModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount)
    {
        var visiblePartIndex = -1;
        var newOpacity       = 1.0f;

        var Phi                  = 0.5f;
        var BackOpacityThreshold = 0.15f;

        // 現在、表示状態になっているパーツを取得
        for ( var i = beginIndex; i < beginIndex + partGroupCount; ++i )
        {
            var partIndex  = _partGroups[i].PartIndex;
            var paramIndex = _partGroups[i].ParameterIndex;

            if ( model.GetParameterValue(paramIndex) > Epsilon )
            {
                if ( visiblePartIndex >= 0 )
                {
                    break;
                }

                visiblePartIndex = i;
                newOpacity       = model.GetPartOpacity(partIndex);

                // 新しい不透明度を計算
                newOpacity += deltaTimeSeconds / _fadeTimeSeconds;

                if ( newOpacity > 1.0f )
                {
                    newOpacity = 1.0f;
                }
            }
        }

        if ( visiblePartIndex < 0 )
        {
            visiblePartIndex = 0;
            newOpacity       = 1.0f;
        }

        //  表示パーツ、非表示パーツの不透明度を設定する
        for ( var i = beginIndex; i < beginIndex + partGroupCount; ++i )
        {
            var partsIndex = _partGroups[i].PartIndex;

            //  表示パーツの設定
            if ( visiblePartIndex == i )
            {
                model.SetPartOpacity(partsIndex, newOpacity); // 先に設定
            }
            // 非表示パーツの設定
            else
            {
                var   opacity = model.GetPartOpacity(partsIndex);
                float a1; // 計算によって求められる不透明度

                if ( newOpacity < Phi )
                {
                    a1 = newOpacity * (Phi - 1) / Phi + 1.0f; // (0,1),(phi,phi)を通る直線式
                }
                else
                {
                    a1 = (1 - newOpacity) * Phi / (1.0f - Phi); // (1,0),(phi,phi)を通る直線式
                }

                // 背景の見える割合を制限する場合
                var backOpacity = (1.0f - a1) * (1.0f - newOpacity);

                if ( backOpacity > BackOpacityThreshold )
                {
                    a1 = 1.0f - BackOpacityThreshold / (1.0f - newOpacity);
                }

                if ( opacity > a1 )
                {
                    opacity = a1; // 計算の不透明度よりも大きければ（濃ければ）不透明度を上げる
                }

                model.SetPartOpacity(partsIndex, opacity);
            }
        }
    }
}

using System.Text;
using System.Text.RegularExpressions;

using Microsoft.Extensions.Logging;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     High-performance sentence segmentation using both rule-based and ML approaches with
///     advanced handling of edge cases and text structures
/// </summary>
public partial class SentenceSegmenter(IMlSentenceDetector mlDetector, ILogger<SentenceSegmenter> logger) : ISentenceSegmenter
{
    private const int MinimumSentences = 2;

    private static readonly Regex SpecialTokenPattern = new(@"\[[A-Z]+:[^\]]+\]", RegexOptions.Compiled);

    private readonly ILogger<SentenceSegmenter> _logger = logger ?? throw new ArgumentNullException(nameof(logger));

    private readonly IMlSentenceDetector _mlDetector = mlDetector ?? throw new ArgumentNullException(nameof(mlDetector));

    /// <summary>
    ///     Segments text into sentences with optimized handling and pre-processing for boundary detection
    /// </summary>
    public IReadOnlyList<string> Segment(string text)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return Array.Empty<string>();
        }

        try
        {
            // First attempt with original text
            var sentences = _mlDetector.Detect(text);

            // If we have fewer than minimum required sentences, try pre-processing
            if ( sentences.Count < MinimumSentences )
            {
                _logger.LogTrace("Initial segmentation yielded only {count} sentence(s), attempting pre-processing", sentences.Count);

                // Apply pre-processing to create potential sentence boundaries
                var preprocessedText = AddPotentialSentenceBoundaries(text);

                // Re-detect sentences with the pre-processed text
                sentences = _mlDetector.Detect(preprocessedText);

                _logger.LogTrace("After pre-processing, segmentation yielded {count} sentence(s)", sentences.Count);
            }

            return sentences;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in sentence segmentation.");

            return Array.Empty<string>();
        }
    }

    /// <summary>
    ///     Adds potential sentence boundaries by replacing certain punctuation with periods
    ///     to help generate at least 2 sentences when possible
    /// </summary>
    private string AddPotentialSentenceBoundaries(string text)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return text;
        }

        try
        {
            if ( !SpecialTokenPattern.IsMatch(text) )
            {
                // No special tokens - apply normal processing
                return ApplyBasicPreprocessing(text);
            }

            var result          = new StringBuilder(text.Length + 20);
            var currentPosition = 0;

            foreach ( Match match in SpecialTokenPattern.Matches(text) )
            {
                // Process the text between the last token and this one
                if ( match.Index > currentPosition )
                {
                    var segment = text.Substring(currentPosition, match.Index - currentPosition);
                    result.Append(ApplyBasicPreprocessing(segment));
                }

                // Append the token unchanged
                result.Append(match.Value);
                currentPosition = match.Index + match.Length;
            }

            if ( currentPosition < text.Length )
            {
                var segment = text[currentPosition..];
                result.Append(ApplyBasicPreprocessing(segment));
            }

            return result.ToString();
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Error during preprocessing for sentence boundaries. Using original text.");

            return text;
        }
    }

    /// <summary>
    ///     Applies basic preprocessing rules to a text segment without special tokens
    /// </summary>
    private string ApplyBasicPreprocessing(string text)
    {
        // Replace semicolons with periods
        text = text.Replace(";", ".");

        // Replace colons with periods
        text = text.Replace(":", ".");

        // Replace dashes with periods (when surrounded by spaces)
        text = text.Replace(" - ", ". ");
        text = text.Replace(" – ", ". ");
        text = text.Replace(" — ", ". ");

        // Ensure proper spacing after periods for the ML detector
        text = TextAfterSpaceRegex().Replace(text, ". $1");

        return text;
    }

    [GeneratedRegex(@"\.([A-Za-z0-9])")] private static partial Regex TextAfterSpaceRegex();
}

using Microsoft.Extensions.Options;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.UI;
using PersonaEngine.Lib.UI.Common;
using PersonaEngine.Lib.UI.GUI;
using PersonaEngine.Lib.UI.Spout;

using Silk.NET.Input;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

namespace PersonaEngine.Lib.Core;

public class AvatarApp : IDisposable
{
    private readonly IOptions<AvatarAppConfig> _config;

    private readonly IConversationOrchestrator _conversationOrchestrator;

    private readonly IReadOnlyList<IRenderComponent> _regularComponents;

    private readonly Dictionary<string, List<IRenderComponent>> _spoutComponents = new();

    private readonly IReadOnlyList<IStartupTask> _startupTasks;

    private readonly IWindow _window;

    private readonly WindowConfiguration _windowConfig;

    private readonly WindowManager _windowManager;

    private GL _gl;

    private ImGuiController _imGui;

    private IInputContext _inputContext;

    private SpoutRegistry _spoutRegistry;

    public AvatarApp(IOptions<AvatarAppConfig> config, IEnumerable<IRenderComponent> renderComponents, IEnumerable<IStartupTask> startupTasks, IConversationOrchestrator conversationOrchestrator)
    {
        _config                   = config;
        _conversationOrchestrator = conversationOrchestrator;

        var allComponents = renderComponents.OrderByDescending(x => x.Priority).ToList();

        // Group components by spout target
        _regularComponents = allComponents.Where(x => !x.UseSpout).ToList();

        // Group spout components by their target
        foreach ( var component in allComponents.Where(x => x.UseSpout) )
        {
            if ( !_spoutComponents.TryGetValue(component.SpoutTarget, out var componentList) )
            {
                componentList                           = new List<IRenderComponent>();
                _spoutComponents[component.SpoutTarget] = componentList;
            }

            componentList.Add(component);
        }

        _windowConfig  = _config.Value.Window;
        _windowManager = new WindowManager(new Vector2D<int>(_windowConfig.Width, _windowConfig.Height), _windowConfig.Title);
        _window        = _windowManager.MainWindow;

        _windowManager.Load        += OnLoad;
        _windowManager.Update      += OnUpdate;
        _windowManager.RenderFrame += OnRender;
        _windowManager.Resize      += OnResize;
        _windowManager.Close       += OnClose;

        _startupTasks = startupTasks.ToList();
    }

    public void Dispose()
    {
        // Context is destroyed anyway when app closes.

        return;

        _spoutRegistry?.Dispose();
        _imGui.Dispose();
    }

    private void OnLoad()
    {
        // Set up keyboard input.
        _inputContext = _window.CreateInput();
        foreach ( var keyboard in _inputContext.Keyboards )
        {
            keyboard.KeyDown += OnKeyDown;
        }

        _gl = _windowManager.GL;

        foreach ( var task in _startupTasks )
        {
            task.Execute(_gl);
        }

        _spoutRegistry = new SpoutRegistry(_gl, _config.Value.SpoutConfigs);

        _imGui = new ImGuiController(_gl, _window, _inputContext, Path.Combine(@"Resources\Fonts", @"Montserrat-Medium.ttf"), Path.Combine(@"Resources\Fonts", @"seguiemj.ttf"));

        InitializeComponents(_regularComponents);

        foreach ( var componentGroup in _spoutComponents.Values )
        {
            InitializeComponents(componentGroup);
        }

        _ = _conversationOrchestrator.StartNewSessionAsync();
    }

    private void InitializeComponents(IEnumerable<IRenderComponent> components)
    {
        foreach ( var component in components )
        {
            component.Initialize(_gl, _window, _inputContext);
        }
    }

    private void OnUpdate(double deltaTime)
    {
        // Update all components
        UpdateComponents(_regularComponents, (float)deltaTime);

        foreach ( var componentGroup in _spoutComponents.Values )
        {
            UpdateComponents(componentGroup, (float)deltaTime);
        }
    }

    private void UpdateComponents(IEnumerable<IRenderComponent> components, float deltaTime)
    {
        foreach ( var component in components )
        {
            component.Update(deltaTime);
        }
    }

    private void OnRender(double deltaTime)
    {
        _imGui.Update((float)deltaTime);
        _gl.Viewport(0, 0, (uint)_windowConfig.Width, (uint)_windowConfig.Height);
        _gl.Clear((uint)ClearBufferMask.ColorBufferBit);

        // Render regular components to the screen
        foreach ( var component in _regularComponents )
        {
            component.Render((float)deltaTime);
        }

        _imGui.Render();

        // Render components to their respective Spout outputs
        foreach ( var spoutGroup in _spoutComponents )
        {
            var spoutTarget = spoutGroup.Key;
            var components  = spoutGroup.Value;

            // Begin frame for this spout target
            _spoutRegistry.BeginFrame(spoutTarget);

            // Render all components for this spout target
            foreach ( var component in components )
            {
                component.Render((float)deltaTime);
            }

            _spoutRegistry.SendFrame(spoutTarget);
        }
    }

    private void OnResize(Vector2D<int> size)
    {
        // Resize all components
        ResizeComponents(_regularComponents);
    }

    private void ResizeComponents(IEnumerable<IRenderComponent> components)
    {
        foreach ( var component in components )
        {
            component.Resize();
        }
    }

    private void OnClose() { }

    private void OnKeyDown(IKeyboard keyboard, Key key, int scancode)
    {
        switch ( key )
        {
            case Key.Escape:
                _window.Close();

                break;
        }
    }

    public void Run() { _windowManager.Run(); }
}

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
///     Audio filter that attaches emotion data to segments during processing
/// </summary>
public class EmotionAudioFilter : IAudioFilter
{
    private readonly IEmotionService _emotionService;

    private readonly ILogger<EmotionAudioFilter> _logger;

    public EmotionAudioFilter(IEmotionService emotionService, ILoggerFactory loggerFactory)
    {
        _emotionService = emotionService ?? throw new ArgumentNullException(nameof(emotionService));
        _logger = loggerFactory?.CreateLogger<EmotionAudioFilter>() ??
                  throw new ArgumentNullException(nameof(loggerFactory));
    }

    /// <summary>
    ///     Priority of the filter (should run after other filters)
    /// </summary>
    public int Priority => -100;

    /// <summary>
    ///     Processes the audio segment to attach emotion data
    /// </summary>
    public void Process(AudioSegment segment)
    {
        // No modification to the audio data is needed
        // This filter is just here to hook into the audio processing pipeline
        // and could be used to perform any additional emotion-related processing if needed

        var emotions = _emotionService.GetEmotions(segment.Id);
        if ( emotions.Any() )
        {
            _logger.LogDebug("Audio segment {SegmentId} has {Count} emotions", segment.Id, emotions.Count);

            // The emotions are already registered with the emotion service
            // We could attach them directly to the segment if needed via a custom extension method
            // or simply document that they should be retrieved via the service
        }
    }
}

using Microsoft.Extensions.Logging;

namespace PersonaEngine.Lib.TTS.Profanity;

/// <summary>
///     A profanity detector that uses an ONNX model under the hood.
///     It supports DI for logging, configurable filter threshold, and a benchmarking utility.
/// </summary>
public class ProfanityDetector : IDisposable
{
    private readonly ILogger<ProfanityDetector> _logger;

    private readonly ProfanityDetectorOnnx _onnxDetector;

    /// <summary>
    ///     Initializes a new instance of the <see cref="ProfanityDetector" /> class.
    /// </summary>
    /// <param name="logger">The logger injected via dependency injection.</param>
    /// <param name="modelPath">Optional path to the ONNX model file.</param>
    /// <param name="vocabPath">Optional path to the vocabulary file for tokenization.</param>
    public ProfanityDetector(ILogger<ProfanityDetector> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _logger.LogInformation("Initializing ProfanityDetector");
        _onnxDetector = new ProfanityDetectorOnnx();
    }

    /// <summary>
    ///     Gets or sets the threshold for detecting any profanity.
    ///     If the model's score is below this threshold, the sentence is considered clean.
    /// </summary>
    public float FilterThreshold { get; set; } = 0.5f;

    /// <summary>
    ///     Gets or sets the threshold for moderate profanity.
    /// </summary>
    public float ModerateThreshold { get; set; } = 0.75f;

    /// <summary>
    ///     Gets or sets the threshold for severe profanity.
    /// </summary>
    public float SevereThreshold { get; set; } = 0.9f;

    public void Dispose() { _onnxDetector.Dispose(); }

    /// <summary>
    ///     Evaluates the given sentence and determines the severity of profanity.
    /// </summary>
    /// <param name="sentence">The sentence to evaluate.</param>
    /// <returns>
    ///     A <see cref="ProfanitySeverity" /> enum value indicating the level of profanity:
    ///     Clean, Mild, Moderate, or Severe.
    /// </returns>
    public ProfanitySeverity EvaluateProfanity(string sentence)
    {
        if ( string.IsNullOrWhiteSpace(sentence) )
        {
            _logger.LogWarning("Empty or whitespace sentence provided.");

            return ProfanitySeverity.Clean;
        }

        var score = _onnxDetector.Run(sentence);
        _logger.LogDebug("Sentence: \"{sentence}\" scored {score}.", sentence, score);

        if ( score < FilterThreshold )
        {
            return ProfanitySeverity.Clean;
        }

        if ( score < ModerateThreshold )
        {
            return ProfanitySeverity.Mild;
        }

        if ( score < SevereThreshold )
        {
            return ProfanitySeverity.Moderate;
        }

        return ProfanitySeverity.Severe;
    }

    /// <summary>
    ///     Benchmarks the profanity filter using provided test data.
    ///     Each test item consists of a sentence and the expected outcome (true if profane, false otherwise).
    ///     For benchmarking purposes, any result other than <see cref="ProfanitySeverity.Clean" /> is considered profane.
    ///     Returns useful statistics to help calibrate the filter thresholds.
    /// </summary>
    /// <param name="testData">A collection of test sentences with their expected results.</param>
    /// <returns>A <see cref="BenchmarkResult" /> instance containing detailed metrics.</returns>
    public BenchmarkResult Benchmark(IEnumerable<(string Sentence, bool ExpectedIsProfane)> testData)
    {
        if ( testData == null )
        {
            throw new ArgumentNullException(nameof(testData));
        }

        int total          = 0,
            truePositives  = 0,
            falsePositives = 0,
            trueNegatives  = 0,
            falseNegatives = 0;

        foreach ( var (sentence, expected) in testData )
        {
            total++;
            var severity = EvaluateProfanity(sentence);
            // For benchmarking, any severity other than Clean is considered profane.
            var actual = severity != ProfanitySeverity.Clean;

            if ( expected && actual )
            {
                truePositives++;
            }
            else if ( !expected && actual )
            {
                falsePositives++;
            }
            else if ( !expected && !actual )
            {
                trueNegatives++;
            }
            else if ( expected && !actual )
            {
                falseNegatives++;
            }

            _logger.LogDebug("Benchmarking sentence: \"{sentence}\" | Severity: {severity} | Expected: {expected} | Actual: {actual}",
                             sentence, severity, expected, actual);
        }

        var accuracy  = total > 0 ? (double)(truePositives + trueNegatives) / total : 0;
        var precision = truePositives + falsePositives > 0 ? (double)truePositives / (truePositives + falsePositives) : 0;
        var recall    = truePositives + falseNegatives > 0 ? (double)truePositives / (truePositives + falseNegatives) : 0;

        var result = new BenchmarkResult {
                                             Total          = total,
                                             TruePositives  = truePositives,
                                             FalsePositives = falsePositives,
                                             TrueNegatives  = trueNegatives,
                                             FalseNegatives = falseNegatives,
                                             Accuracy       = accuracy,
                                             Precision      = precision,
                                             Recall         = recall
                                         };

        _logger.LogInformation("Benchmark results: {result} [Thresholds: FilterThreshold={FilterThreshold}, ModerateThreshold={ModerateThreshold}, SevereThreshold={SevereThreshold}]",
                               result, FilterThreshold, ModerateThreshold, SevereThreshold);

        return result;
    }
}

using System.Numerics;

using FontStashSharp.Interfaces;

using PersonaEngine.Lib.UI.Common;

using Silk.NET.OpenGL;

using Shader = PersonaEngine.Lib.UI.Common.Shader;
using Texture = PersonaEngine.Lib.UI.Common.Texture;

namespace PersonaEngine.Lib.UI.Text.Rendering;

internal class TextRenderer : IFontStashRenderer2, IDisposable
{
    private const int MAX_SPRITES = 2048;

    private const int MAX_VERTICES = MAX_SPRITES * 4;

    private const int MAX_INDICES = MAX_SPRITES * 6;

    private static readonly short[] indexData = GenerateIndexArray();

    private readonly GL _gl;

    private readonly BufferObject<short> _indexBuffer;

    private readonly Shader _shader;

    private readonly Texture2DManager _textureManager;

    private readonly VertexArrayObject _vao;

    private readonly BufferObject<VertexPositionColorTexture> _vertexBuffer;

    private readonly VertexPositionColorTexture[] _vertexData = new VertexPositionColorTexture[MAX_VERTICES];

    private object _lastTexture;

    private int _vertexIndex = 0;

    private int _viewportHeight = 800;

    private int _viewportWidth = 1200;

    public unsafe TextRenderer(GL glApi)
    {
        _gl = glApi;

        _textureManager = new Texture2DManager(_gl);

        _vertexBuffer = new BufferObject<VertexPositionColorTexture>(_gl, MAX_VERTICES, BufferTargetARB.ArrayBuffer, true);
        _indexBuffer  = new BufferObject<short>(_gl, indexData.Length, BufferTargetARB.ElementArrayBuffer, false);
        _indexBuffer.SetData(indexData, 0, indexData.Length);

        var vertSrc = File.ReadAllText(Path.Combine(@"Resources/Shaders", "t_shader.vert"));
        var fragSrc = File.ReadAllText(Path.Combine(@"Resources/Shaders", "t_shader.frag"));
        _shader = new Shader(_gl, vertSrc, fragSrc);
        _shader.Use();

        _vao = new VertexArrayObject(_gl, sizeof(VertexPositionColorTexture));
        _vao.Bind();

        var location = _shader.GetAttribLocation("a_position");
        _vao.VertexAttribPointer(location, 3, VertexAttribPointerType.Float, false, 0);

        location = _shader.GetAttribLocation("a_color");
        _vao.VertexAttribPointer(location, 4, VertexAttribPointerType.UnsignedByte, true, 12);

        location = _shader.GetAttribLocation("a_texCoords0");
        _vao.VertexAttribPointer(location, 2, VertexAttribPointerType.Float, false, 16);
    }

    public void Dispose() { Dispose(true); }

    public ITexture2DManager TextureManager => _textureManager;

    public void DrawQuad(object texture, ref VertexPositionColorTexture topLeft, ref VertexPositionColorTexture topRight, ref VertexPositionColorTexture bottomLeft, ref VertexPositionColorTexture bottomRight)
    {
        if ( _lastTexture != texture )
        {
            FlushBuffer();
        }

        _vertexData[_vertexIndex++] = topLeft;
        _vertexData[_vertexIndex++] = topRight;
        _vertexData[_vertexIndex++] = bottomLeft;
        _vertexData[_vertexIndex++] = bottomRight;

        _lastTexture = texture;
    }

    ~TextRenderer() { Dispose(false); }

    protected virtual void Dispose(bool disposing)
    {
        if ( !disposing )
        {
            return;
        }

        _vao.Dispose();
        _vertexBuffer.Dispose();
        _indexBuffer.Dispose();
        _shader.Dispose();
    }

    public void Begin()
    {
        _gl.Disable(EnableCap.DepthTest);
        _gl.CheckError();
        _gl.Enable(EnableCap.Blend);
        _gl.CheckError();
        _gl.BlendFunc(BlendingFactor.One, BlendingFactor.OneMinusSrcAlpha);
        _gl.CheckError();

        _shader.Use();
        _shader.SetUniform("TextureSampler", 0);

        var transform = Matrix4x4.CreateOrthographicOffCenter(0, _viewportWidth, _viewportHeight, 0, 0, -1);
        _shader.SetUniform("MatrixTransform", transform);

        _vao.Bind();
        _indexBuffer.Bind();
        _vertexBuffer.Bind();
    }

    public void End() { FlushBuffer(); }

    private void FlushBuffer()
    {
        unsafe
        {
            if ( _vertexIndex == 0 || _lastTexture == null )
            {
                return;
            }

            _vertexBuffer.SetData(_vertexData, 0, _vertexIndex);

            var texture = (Texture)_lastTexture;
            texture.Bind();

            _gl.DrawElements(PrimitiveType.Triangles, (uint)(_vertexIndex * 6 / 4), DrawElementsType.UnsignedShort, null);
            _vertexIndex = 0;
        }
    }

    private static short[] GenerateIndexArray()
    {
        var result = new short[MAX_INDICES];
        for ( int i = 0,
                  j = 0; i < MAX_INDICES; i += 6, j += 4 )
        {
            result[i]     = (short)j;
            result[i + 1] = (short)(j + 1);
            result[i + 2] = (short)(j + 2);
            result[i + 3] = (short)(j + 3);
            result[i + 4] = (short)(j + 2);
            result[i + 5] = (short)(j + 1);
        }

        return result;
    }

    public void OnViewportChanged(int width, int height)
    {
        _viewportWidth  = width;
        _viewportHeight = height;
    }
}

using System.Numerics;

using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;
using PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;
using PersonaEngine.Lib.Live2D.Framework.Type;

namespace PersonaEngine.Lib.Live2D.Framework.Rendering;

public class CubismClippingManager
{
    /// <summary>
    ///     実験時に1チャンネルの場合は1、RGBだけの場合は3、アルファも含める場合は4
    /// </summary>
    public const int ColorChannelCount = 4;

    /// <summary>
    ///     通常のフレームバッファ1枚あたりのマスク最大数
    /// </summary>
    public const int ClippingMaskMaxCountOnDefault = 36;

    /// <summary>
    ///     フレームバッファが2枚以上ある場合のフレームバッファ1枚あたりのマスク最大数
    /// </summary>
    public const int ClippingMaskMaxCountOnMultiRenderTexture = 32;

    private readonly RenderType _renderType;

    protected List<CubismTextureColor> ChannelColors = [];

    /// <summary>
    ///     マスクのクリアフラグの配列
    /// </summary>
    protected List<bool> ClearedMaskBufferFlags = [];

    /// <summary>
    ///     マスク用クリッピングコンテキストのリスト
    /// </summary>
    protected List<CubismClippingContext> ClippingContextListForMask = [];

    /// <summary>
    ///     オフスクリーンサーフェイスのアドレス
    /// </summary>
    protected CubismOffscreenSurface_OpenGLES2 CurrentMaskBuffer;

    /// <summary>
    ///     マスク配置計算用の矩形
    /// </summary>
    protected RectF TmpBoundsOnModel = new();

    /// <summary>
    ///     マスク計算用の行列
    /// </summary>
    protected CubismMatrix44 TmpMatrix = new();

    /// <summary>
    ///     マスク計算用の行列
    /// </summary>
    protected CubismMatrix44 TmpMatrixForDraw = new();

    /// <summary>
    ///     マスク計算用の行列
    /// </summary>
    protected CubismMatrix44 TmpMatrixForMask = new();

    public CubismClippingManager(RenderType render)
    {
        _renderType            = render;
        ClippingMaskBufferSize = new Vector2(256, 256);

        ChannelColors.Add(new CubismTextureColor(1.0f, 0f, 0f, 0f));
        ChannelColors.Add(new CubismTextureColor(0f, 1.0f, 0f, 0f));
        ChannelColors.Add(new CubismTextureColor(0f, 0f, 1.0f, 0f));
        ChannelColors.Add(new CubismTextureColor(0f, 0f, 0f, 1.0f));
    }

    /// <summary>
    ///     描画用クリッピングコンテキストのリスト
    /// </summary>
    public List<CubismClippingContext?> ClippingContextListForDraw { get; init; } = [];

    /// <summary>
    ///     クリッピングマスクのバッファサイズ（初期値:256）
    /// </summary>
    public Vector2 ClippingMaskBufferSize { get; private set; }

    /// <summary>
    ///     生成するレンダーテクスチャの枚数
    /// </summary>
    public int RenderTextureCount { get; private set; }

    public void Close()
    {
        ClippingContextListForDraw.Clear();
        ClippingContextListForMask.Clear();
        ChannelColors.Clear();
        ClearedMaskBufferFlags.Clear();
    }

    /// <summary>
    ///     マネージャの初期化処理
    ///     クリッピングマスクを使う描画オブジェクトの登録を行う
    /// </summary>
    /// <param name="model">モデルのインスタンス</param>
    /// <param name="maskBufferCount">バッファの生成数</param>
    public unsafe void Initialize(CubismModel model, int maskBufferCount)
    {
        RenderTextureCount = maskBufferCount;

        // レンダーテクスチャのクリアフラグの設定
        for ( var i = 0; i < RenderTextureCount; ++i )
        {
            ClearedMaskBufferFlags.Add(false);
        }

        //クリッピングマスクを使う描画オブジェクトを全て登録する
        //クリッピングマスクは、通常数個程度に限定して使うものとする
        for ( var i = 0; i < model.GetDrawableCount(); i++ )
        {
            if ( model.GetDrawableMaskCounts()[i] <= 0 )
            {
                //クリッピングマスクが使用されていないアートメッシュ（多くの場合使用しない）
                ClippingContextListForDraw.Add(null);

                continue;
            }

            // 既にあるClipContextと同じかチェックする
            var cc = FindSameClip(model.GetDrawableMasks()[i], model.GetDrawableMaskCounts()[i]);
            if ( cc == null )
            {
                // 同一のマスクが存在していない場合は生成する
                if ( _renderType == RenderType.OpenGL )
                {
                    cc = new CubismClippingContext_OpenGLES2(this, model, model.GetDrawableMasks()[i], model.GetDrawableMaskCounts()[i]);
                }
                else
                {
                    throw new Exception("Only OpenGL");
                }

                ClippingContextListForMask.Add(cc);
            }

            cc.AddClippedDrawable(i);

            ClippingContextListForDraw.Add(cc);
        }
    }

    /// <summary>
    ///     既にマスクを作っているかを確認。
    ///     作っているようであれば該当するクリッピングマスクのインスタンスを返す。
    ///     作っていなければNULLを返す
    /// </summary>
    /// <param name="drawableMasks">描画オブジェクトをマスクする描画オブジェクトのリスト</param>
    /// <param name="drawableMaskCounts">描画オブジェクトをマスクする描画オブジェクトの数</param>
    /// <returns>該当するクリッピングマスクが存在すればインスタンスを返し、なければNULLを返す。</returns>
    private unsafe CubismClippingContext? FindSameClip(int* drawableMasks, int drawableMaskCounts)
    {
        // 作成済みClippingContextと一致するか確認
        for ( var i = 0; i < ClippingContextListForMask.Count; i++ )
        {
            var cc    = ClippingContextListForMask[i];
            var count = cc.ClippingIdCount;
            if ( count != drawableMaskCounts )
            {
                continue; //個数が違う場合は別物
            }

            var samecount = 0;

            // 同じIDを持つか確認。配列の数が同じなので、一致した個数が同じなら同じ物を持つとする。
            for ( var j = 0; j < count; j++ )
            {
                var clipId = cc.ClippingIdList[j];
                for ( var k = 0; k < count; k++ )
                {
                    if ( drawableMasks[k] == clipId )
                    {
                        samecount++;

                        break;
                    }
                }
            }

            if ( samecount == count )
            {
                return cc;
            }
        }

        return null; //見つからなかった
    }

    /// <summary>
    ///     高精細マスク処理用の行列を計算する
    /// </summary>
    /// <param name="model">モデルのインスタンス</param>
    /// <param name="isRightHanded">処理が右手系であるか</param>
    public void SetupMatrixForHighPrecision(CubismModel model, bool isRightHanded)
    {
        // 全てのクリッピングを用意する
        // 同じクリップ（複数の場合はまとめて１つのクリップ）を使う場合は１度だけ設定する
        var usingClipCount = 0;
        for ( var clipIndex = 0; clipIndex < ClippingContextListForMask.Count; clipIndex++ )
        {
            // １つのクリッピングマスクに関して
            var cc = ClippingContextListForMask[clipIndex];

            // このクリップを利用する描画オブジェクト群全体を囲む矩形を計算
            CalcClippedDrawTotalBounds(model, cc);

            if ( cc.IsUsing )
            {
                usingClipCount++; //使用中としてカウント
            }
        }

        if ( usingClipCount <= 0 )
        {
            return;
        }

        // マスク行列作成処理
        SetupLayoutBounds(0);

        // サイズがレンダーテクスチャの枚数と合わない場合は合わせる
        if ( ClearedMaskBufferFlags.Count != RenderTextureCount )
        {
            ClearedMaskBufferFlags.Clear();

            for ( var i = 0; i < RenderTextureCount; ++i )
            {
                ClearedMaskBufferFlags.Add(false);
            }
        }
        else
        {
            // マスクのクリアフラグを毎フレーム開始時に初期化
            for ( var i = 0; i < RenderTextureCount; ++i )
            {
                ClearedMaskBufferFlags[i] = false;
            }
        }

        // 実際にマスクを生成する
        // 全てのマスクをどの様にレイアウトして描くかを決定し、ClipContext , ClippedDrawContext に記憶する
        for ( var clipIndex = 0; clipIndex < ClippingContextListForMask.Count; clipIndex++ )
        {
            // --- 実際に１つのマスクを描く ---
            var   clipContext         = ClippingContextListForMask[clipIndex];
            var   allClippedDrawRect  = clipContext.AllClippedDrawRect; //このマスクを使う、全ての描画オブジェクトの論理座標上の囲み矩形
            var   layoutBoundsOnTex01 = clipContext.LayoutBounds;       //この中にマスクを収める
            var   MARGIN              = 0.05f;
            float scaleX;
            float scaleY;
            var   ppu                = model.GetPixelsPerUnit();
            var   maskPixelWidth     = clipContext.Manager.ClippingMaskBufferSize.X;
            var   maskPixelHeight    = clipContext.Manager.ClippingMaskBufferSize.Y;
            var   physicalMaskWidth  = layoutBoundsOnTex01.Width * maskPixelWidth;
            var   physicalMaskHeight = layoutBoundsOnTex01.Height * maskPixelHeight;

            TmpBoundsOnModel.SetRect(allClippedDrawRect);
            if ( TmpBoundsOnModel.Width * ppu > physicalMaskWidth )
            {
                TmpBoundsOnModel.Expand(allClippedDrawRect.Width * MARGIN, 0.0f);
                scaleX = layoutBoundsOnTex01.Width / TmpBoundsOnModel.Width;
            }
            else
            {
                scaleX = ppu / physicalMaskWidth;
            }

            if ( TmpBoundsOnModel.Height * ppu > physicalMaskHeight )
            {
                TmpBoundsOnModel.Expand(0.0f, allClippedDrawRect.Height * MARGIN);
                scaleY = layoutBoundsOnTex01.Height / TmpBoundsOnModel.Height;
            }
            else
            {
                scaleY = ppu / physicalMaskHeight;
            }

            // マスク生成時に使う行列を求める
            CreateMatrixForMask(isRightHanded, layoutBoundsOnTex01, scaleX, scaleY);

            clipContext.MatrixForMask.SetMatrix(TmpMatrixForMask.Tr);
            clipContext.MatrixForDraw.SetMatrix(TmpMatrixForDraw.Tr);
        }
    }

    /// <summary>
    ///     マスク作成・描画用の行列を作成する。
    /// </summary>
    /// <param name="isRightHanded">座標を右手系として扱うかを指定</param>
    /// <param name="layoutBoundsOnTex01">マスクを収める領域</param>
    /// <param name="scaleX">描画オブジェクトの伸縮率</param>
    /// <param name="scaleY">描画オブジェクトの伸縮率</param>
    protected void CreateMatrixForMask(bool isRightHanded, RectF layoutBoundsOnTex01, float scaleX, float scaleY)
    {
        TmpMatrix.LoadIdentity();
        {
            // Layout0..1 を -1..1に変換
            TmpMatrix.TranslateRelative(-1.0f, -1.0f);
            TmpMatrix.ScaleRelative(2.0f, 2.0f);
        }

        {
            // view to Layout0..1
            TmpMatrix.TranslateRelative(layoutBoundsOnTex01.X, layoutBoundsOnTex01.Y); //new = [translate]
            TmpMatrix.ScaleRelative(scaleX, scaleY);                                   //new = [translate][scale]
            TmpMatrix.TranslateRelative(-TmpBoundsOnModel.X, -TmpBoundsOnModel.Y);     //new = [translate][scale][translate]
        }

        // tmpMatrixForMask が計算結果
        TmpMatrixForMask.SetMatrix(TmpMatrix.Tr);

        TmpMatrix.LoadIdentity();
        {
            TmpMatrix.TranslateRelative(layoutBoundsOnTex01.X, layoutBoundsOnTex01.Y * (isRightHanded ? -1.0f : 1.0f)); //new = [translate]
            TmpMatrix.ScaleRelative(scaleX, scaleY * (isRightHanded ? -1.0f : 1.0f));                                   //new = [translate][scale]
            TmpMatrix.TranslateRelative(-TmpBoundsOnModel.X, -TmpBoundsOnModel.Y);                                      //new = [translate][scale][translate]
        }

        TmpMatrixForDraw.SetMatrix(TmpMatrix.Tr);
    }

    /// <summary>
    ///     クリッピングコンテキストを配置するレイアウト。
    ///     ひとつのレンダーテクスチャを極力いっぱいに使ってマスクをレイアウトする。
    ///     マスクグループの数が4以下ならRGBA各チャンネルに１つずつマスクを配置し、5以上6以下ならRGBAを2,2,1,1と配置する。
    /// </summary>
    /// <param name="usingClipCount">配置するクリッピングコンテキストの数</param>
    protected void SetupLayoutBounds(int usingClipCount)
    {
        var useClippingMaskMaxCount = RenderTextureCount <= 1
                                          ? ClippingMaskMaxCountOnDefault
                                          : ClippingMaskMaxCountOnMultiRenderTexture * RenderTextureCount;

        if ( usingClipCount <= 0 || usingClipCount > useClippingMaskMaxCount )
        {
            if ( usingClipCount > useClippingMaskMaxCount )
            {
                // マスクの制限数の警告を出す
                var count = usingClipCount - useClippingMaskMaxCount;
                CubismLog.Error("not supported mask count : %d\n[Details] render texture count: %d\n, mask count : %d"
                              , count, RenderTextureCount, usingClipCount);
            }

            // この場合は一つのマスクターゲットを毎回クリアして使用する
            for ( var index = 0; index < ClippingContextListForMask.Count; index++ )
            {
                var cc = ClippingContextListForMask[index];
                cc.LayoutChannelIndex  = 0; // どうせ毎回消すので固定で良い
                cc.LayoutBounds.X      = 0.0f;
                cc.LayoutBounds.Y      = 0.0f;
                cc.LayoutBounds.Width  = 1.0f;
                cc.LayoutBounds.Height = 1.0f;
                cc.BufferIndex         = 0;
            }

            return;
        }

        // レンダーテクスチャが1枚なら9分割する（最大36枚）
        var layoutCountMaxValue = RenderTextureCount <= 1 ? 9 : 8;

        // ひとつのRenderTextureを極力いっぱいに使ってマスクをレイアウトする
        // マスクグループの数が4以下ならRGBA各チャンネルに１つずつマスクを配置し、5以上6以下ならRGBAを2,2,1,1と配置する
        var countPerSheetDiv         = (usingClipCount + RenderTextureCount - 1) / RenderTextureCount; // レンダーテクスチャ1枚あたり何枚割り当てるか（切り上げ）
        var reduceLayoutTextureCount = usingClipCount % RenderTextureCount;                            // レイアウトの数を1枚減らすレンダーテクスチャの数（この数だけのレンダーテクスチャが対象）

        // RGBAを順番に使っていく
        var divCount = countPerSheetDiv / ColorChannelCount; //１チャンネルに配置する基本のマスク個数
        var modCount = countPerSheetDiv % ColorChannelCount; //余り、この番号のチャンネルまでに１つずつ配分する

        // RGBAそれぞれのチャンネルを用意していく(0:R , 1:G , 2:B, 3:A, )
        var curClipIndex = 0; //順番に設定していく

        for ( var renderTextureIndex = 0; renderTextureIndex < RenderTextureCount; renderTextureIndex++ )
        {
            for ( var channelIndex = 0; channelIndex < ColorChannelCount; channelIndex++ )
            {
                // このチャンネルにレイアウトする数
                // NOTE: レイアウト数 = 1チャンネルに配置する基本のマスク + 余りのマスクを置くチャンネルなら1つ追加
                var layoutCount = divCount + (channelIndex < modCount ? 1 : 0);

                // レイアウトの数を1枚減らす場合にそれを行うチャンネルを決定
                // divが0の時は正常なインデックスの範囲内になるように調整
                var checkChannelIndex = modCount + (divCount < 1 ? -1 : 0);

                // 今回が対象のチャンネルかつ、レイアウトの数を1枚減らすレンダーテクスチャが存在する場合
                if ( channelIndex == checkChannelIndex && reduceLayoutTextureCount > 0 )
                {
                    // 現在のレンダーテクスチャが、対象のレンダーテクスチャであればレイアウトの数を1枚減らす
                    layoutCount -= !(renderTextureIndex < reduceLayoutTextureCount) ? 1 : 0;
                }

                // 分割方法を決定する
                if ( layoutCount == 0 )
                {
                    // 何もしない
                }
                else if ( layoutCount == 1 )
                {
                    //全てをそのまま使う
                    var cc = ClippingContextListForMask[curClipIndex++];
                    cc.LayoutChannelIndex  = channelIndex;
                    cc.LayoutBounds.X      = 0.0f;
                    cc.LayoutBounds.Y      = 0.0f;
                    cc.LayoutBounds.Width  = 1.0f;
                    cc.LayoutBounds.Height = 1.0f;
                    cc.BufferIndex         = renderTextureIndex;
                }
                else if ( layoutCount == 2 )
                {
                    for ( var i = 0; i < layoutCount; i++ )
                    {
                        var xpos = i % 2;

                        var cc = ClippingContextListForMask[curClipIndex++];
                        cc.LayoutChannelIndex = channelIndex;

                        cc.LayoutBounds.X      = xpos * 0.5f;
                        cc.LayoutBounds.Y      = 0.0f;
                        cc.LayoutBounds.Width  = 0.5f;
                        cc.LayoutBounds.Height = 1.0f;
                        cc.BufferIndex         = renderTextureIndex;
                        //UVを2つに分解して使う
                    }
                }
                else if ( layoutCount <= 4 )
                {
                    //4分割して使う
                    for ( var i = 0; i < layoutCount; i++ )
                    {
                        var xpos = i % 2;
                        var ypos = i / 2;

                        var cc = ClippingContextListForMask[curClipIndex++];
                        cc.LayoutChannelIndex = channelIndex;

                        cc.LayoutBounds.X      = xpos * 0.5f;
                        cc.LayoutBounds.Y      = ypos * 0.5f;
                        cc.LayoutBounds.Width  = 0.5f;
                        cc.LayoutBounds.Height = 0.5f;
                        cc.BufferIndex         = renderTextureIndex;
                    }
                }
                else if ( layoutCount <= layoutCountMaxValue )
                {
                    //9分割して使う
                    for ( var i = 0; i < layoutCount; i++ )
                    {
                        var xpos = i % 3;
                        var ypos = i / 3;

                        var cc = ClippingContextListForMask[curClipIndex++];
                        cc.LayoutChannelIndex = channelIndex;

                        cc.LayoutBounds.X      = xpos / 3.0f;
                        cc.LayoutBounds.Y      = ypos / 3.0f;
                        cc.LayoutBounds.Width  = 1.0f / 3.0f;
                        cc.LayoutBounds.Height = 1.0f / 3.0f;
                        cc.BufferIndex         = renderTextureIndex;
                    }
                }
                // マスクの制限枚数を超えた場合の処理
                else
                {
                    var count = usingClipCount - useClippingMaskMaxCount;

                    // 開発モードの場合は停止させる
                    throw new Exception($"not supported mask count : {count}\n[Details] render texture count: {RenderTextureCount}\n, mask count : {usingClipCount}");
                }
            }
        }
    }

    /// <summary>
    ///     マスクされる描画オブジェクト群全体を囲む矩形(モデル座標系)を計算する
    /// </summary>
    /// <param name="model">モデルのインスタンス</param>
    /// <param name="clippingContext">クリッピングマスクのコンテキスト</param>
    protected unsafe void CalcClippedDrawTotalBounds(CubismModel model, CubismClippingContext clippingContext)
    {
        // 被クリッピングマスク（マスクされる描画オブジェクト）の全体の矩形
        float clippedDrawTotalMinX = float.MaxValue,
              clippedDrawTotalMinY = float.MaxValue;

        float clippedDrawTotalMaxX = float.MinValue,
              clippedDrawTotalMaxY = float.MinValue;

        // このマスクが実際に必要か判定する
        // このクリッピングを利用する「描画オブジェクト」がひとつでも使用可能であればマスクを生成する必要がある

        var clippedDrawCount = clippingContext.ClippedDrawableIndexList.Count;
        for ( var clippedDrawableIndex = 0; clippedDrawableIndex < clippedDrawCount; clippedDrawableIndex++ )
        {
            // マスクを使用する描画オブジェクトの描画される矩形を求める
            var drawableIndex = clippingContext.ClippedDrawableIndexList[clippedDrawableIndex];

            var drawableVertexCount = model.GetDrawableVertexCount(drawableIndex);
            var drawableVertexes    = model.GetDrawableVertices(drawableIndex);

            float minX = float.MaxValue,
                  minY = float.MaxValue;

            float maxX = float.MinValue,
                  maxY = float.MinValue;

            var loop = drawableVertexCount * CubismFramework.VertexStep;
            for ( var pi = CubismFramework.VertexOffset; pi < loop; pi += CubismFramework.VertexStep )
            {
                var x = drawableVertexes[pi];
                var y = drawableVertexes[pi + 1];
                if ( x < minX )
                {
                    minX = x;
                }

                if ( x > maxX )
                {
                    maxX = x;
                }

                if ( y < minY )
                {
                    minY = y;
                }

                if ( y > maxY )
                {
                    maxY = y;
                }
            }

            //
            if ( minX == float.MaxValue )
            {
                continue; //有効な点がひとつも取れなかったのでスキップする
            }

            // 全体の矩形に反映
            if ( minX < clippedDrawTotalMinX )
            {
                clippedDrawTotalMinX = minX;
            }

            if ( minY < clippedDrawTotalMinY )
            {
                clippedDrawTotalMinY = minY;
            }

            if ( maxX > clippedDrawTotalMaxX )
            {
                clippedDrawTotalMaxX = maxX;
            }

            if ( maxY > clippedDrawTotalMaxY )
            {
                clippedDrawTotalMaxY = maxY;
            }
        }

        if ( clippedDrawTotalMinX == float.MaxValue )
        {
            clippingContext.AllClippedDrawRect.X      = 0.0f;
            clippingContext.AllClippedDrawRect.Y      = 0.0f;
            clippingContext.AllClippedDrawRect.Width  = 0.0f;
            clippingContext.AllClippedDrawRect.Height = 0.0f;
            clippingContext.IsUsing                   = false;
        }
        else
        {
            clippingContext.IsUsing = true;
            var w = clippedDrawTotalMaxX - clippedDrawTotalMinX;
            var h = clippedDrawTotalMaxY - clippedDrawTotalMinY;
            clippingContext.AllClippedDrawRect.X      = clippedDrawTotalMinX;
            clippingContext.AllClippedDrawRect.Y      = clippedDrawTotalMinY;
            clippingContext.AllClippedDrawRect.Width  = w;
            clippingContext.AllClippedDrawRect.Height = h;
        }
    }

    /// <summary>
    ///     カラーチャンネル(RGBA)のフラグを取得する
    /// </summary>
    /// <param name="channelNo">カラーチャンネル(RGBA)の番号(0:R , 1:G , 2:B, 3:A)</param>
    /// <returns></returns>
    public CubismTextureColor GetChannelFlagAsColor(int channelNo) { return ChannelColors[channelNo]; }

    /// <summary>
    ///     クリッピングマスクバッファのサイズを設定する
    /// </summary>
    /// <param name="width">クリッピングマスクバッファのサイズ</param>
    /// <param name="height">クリッピングマスクバッファのサイズ</param>
    public void SetClippingMaskBufferSize(float width, float height) { ClippingMaskBufferSize = new Vector2(width, height); }
}

using PersonaEngine.Lib.Live2D.Framework;
using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;
using PersonaEngine.Lib.Live2D.Framework.Motion;

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     サンプルアプリケーションにおいてCubismModelを管理するクラス
///     モデル生成と破棄、タップイベントの処理、モデル切り替えを行う。
/// </summary>
/// <remarks>
///     コンストラクタ
/// </remarks>
public class LAppLive2DManager(LAppDelegate lapp) : IDisposable
{
    /// <summary>
    ///     モデルインスタンスのコンテナ
    /// </summary>
    private readonly List<LAppModel> _models = [];

    private readonly CubismMatrix44 _projection = new();

    /// <summary>
    ///     モデル描画に用いるView行列
    /// </summary>
    public CubismMatrix44 ViewMatrix { get; } = new();

    public void Dispose() { ReleaseAllModel(); }

    public event Action<CubismModel, ACubismMotion>? MotionFinished;

    /// <summary>
    ///     現在のシーンで保持しているモデルを返す
    /// </summary>
    /// <param name="no">モデルリストのインデックス値</param>
    /// <returns>モデルのインスタンスを返す。インデックス値が範囲外の場合はNULLを返す。</returns>
    public LAppModel GetModel(int no) { return _models[no]; }

    /// <summary>
    ///     現在のシーンで保持しているすべてのモデルを解放する
    /// </summary>
    public void ReleaseAllModel()
    {
        for ( var i = 0; i < _models.Count; i++ )
        {
            _models[i].Dispose();
        }

        _models.Clear();
    }

    /// <summary>
    ///     画面をドラッグしたときの処理
    /// </summary>
    /// <param name="x">画面のX座標</param>
    /// <param name="y">画面のY座標</param>
    public void OnDrag(float x, float y)
    {
        for ( var i = 0; i < _models.Count; i++ )
        {
            var model = GetModel(i);

            model.SetDragging(x, y);
        }
    }

    /// <summary>
    ///     画面をタップしたときの処理
    /// </summary>
    /// <param name="x">画面のX座標</param>
    /// <param name="y">画面のY座標</param>
    public void OnTap(float x, float y)
    {
        CubismLog.Debug($"[Live2D]tap point: x:{x:0.00} y:{y:0.00}");

        for ( var i = 0; i < _models.Count; i++ )
        {
            if ( _models[i].HitTest(LAppDefine.HitAreaNameHead, x, y) )
            {
                CubismLog.Debug($"[Live2D]hit area: [{LAppDefine.HitAreaNameHead}]");
                _models[i].SetRandomExpression();
            }
            else if ( _models[i].HitTest(LAppDefine.HitAreaNameBody, x, y) )
            {
                CubismLog.Debug($"[Live2D]hit area: [{LAppDefine.HitAreaNameBody}]");
                _models[i].StartRandomMotion(LAppDefine.MotionGroupTapBody, MotionPriority.PriorityNormal, OnFinishedMotion);
            }
        }
    }

    private void OnFinishedMotion(CubismModel model, ACubismMotion self)
    {
        CubismLog.Info($"[Live2D]Motion Finished: {self}");
        MotionFinished?.Invoke(model, self);
    }

    /// <summary>
    ///     画面を更新するときの処理
    ///     モデルの更新処理および描画処理を行う
    /// </summary>
    public void OnUpdate()
    {
        lapp.GL.GetWindowSize(out var width, out var height);

        var modelCount = _models.Count;
        foreach ( var model in _models )
        {
            _projection.LoadIdentity();

            if ( model.Model.GetCanvasWidth() > 1.0f && width < height )
            {
                // 横に長いモデルを縦長ウィンドウに表示する際モデルの横サイズでscaleを算出する
                model.ModelMatrix.SetWidth(2.0f);
                _projection.Scale(1.0f, (float)width / height);
            }
            else
            {
                _projection.Scale((float)height / width, 1.0f);
            }

            // 必要があればここで乗算
            if ( ViewMatrix != null )
            {
                _projection.MultiplyByMatrix(ViewMatrix);
            }

            model.Update();
            model.Draw(_projection); // 参照渡しなのでprojectionは変質する
        }
    }

    public LAppModel LoadModel(string dir, string name)
    {
        CubismLog.Debug($"[Live2D]model load: {name}");

        // ModelDir[]に保持したディレクトリ名から
        // model3.jsonのパスを決定する.
        // ディレクトリ名とmodel3.jsonの名前を一致させておくこと.
        if ( !dir.EndsWith('\\') && !dir.EndsWith('/') )
        {
            dir = Path.GetFullPath(dir + '/');
        }

        var modelJsonName = Path.GetFullPath($"{dir}{name}");
        if ( !File.Exists(modelJsonName) )
        {
            modelJsonName = Path.GetFullPath($"{dir}{name}.model3.json");
        }

        if ( !File.Exists(modelJsonName) )
        {
            dir           = Path.GetFullPath(dir + name + '/');
            modelJsonName = Path.GetFullPath($"{dir}{name}.model3.json");
        }

        if ( !File.Exists(modelJsonName) )
        {
            throw new Exception($"[Live2D]File not found: {modelJsonName}");
        }

        var model = new LAppModel(lapp, dir, modelJsonName);
        _models.Add(model);

        return model;
    }

    public void RemoveModel(int index)
    {
        if ( _models.Count > index )
        {
            var model = _models[index];
            _models.RemoveAt(index);
            model.Dispose();
        }
    }

    /// <summary>
    ///     モデル個数を得る
    /// </summary>
    /// <returns>所持モデル個数</returns>
    public int GetModelNum() { return _models.Count; }

    public async void StartSpeaking(string filePath)
    {
        for ( var i = 0; i < _models.Count; i++ )
        {
            _models[i]._wavFileHandler.Start(filePath);
            await _models[i]._wavFileHandler.LoadWavFile(filePath);
            _models[i].StartMotion(LAppDefine.MotionGroupIdle, 0, MotionPriority.PriorityIdle, OnFinishedMotion);
        }
    }
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents an audio source that can be discard samples at the beginning of the stream if not needed.
/// </summary>
/// <remarks>
///     It can store samples as floats or as bytes, or both. By default, it stores samples as floats.
///     Based on your usage, you can choose to store samples as bytes, floats, or both.
///     If storing them as floats, they will be deserialized from bytes when they are added and returned directly when
///     requested.
///     If storing them as bytes, they will be serialized from floats when they are added and returned directly when
///     requested.
///     If you want to optimize your memory usage, you can store them in the same format as they are added.
///     If you want to optimize your CPU usage, you can store them in the format you want to use them in.
/// </remarks>
public class DiscardableMemoryAudioSource(
    IReadOnlyDictionary<string, string> metadata,
    bool                                storeSamples        = true,
    bool                                storeBytes          = false,
    int                                 initialSizeFloats   = BufferedMemoryAudioSource.DefaultInitialSize,
    int                                 initialSizeBytes    = BufferedMemoryAudioSource.DefaultInitialSize,
    IChannelAggregationStrategy?        aggregationStrategy = null)
    : BufferedMemoryAudioSource(metadata, storeSamples, storeBytes, initialSizeFloats, initialSizeBytes, aggregationStrategy), IDiscardableAudioSource
{
    private long framesDiscarded;

    public long SampleVirtualCount => base.FramesCount;

    public override long FramesCount => base.FramesCount - framesDiscarded;

    public override TimeSpan Duration => TimeSpan.FromMilliseconds(SampleVirtualCount * 1000d / SampleRate);

    /// <inheritdoc />
    public virtual void DiscardFrames(int count)
    {
        ThrowIfNotInitialized();

        if ( count <= 0 )
        {
            throw new ArgumentOutOfRangeException(nameof(count), "Count must be positive.");
        }

        if ( count > FramesCount )
        {
            throw new ArgumentOutOfRangeException(nameof(count), "Cannot discard more samples than available.");
        }

        var framesToKeep = FramesCount - count;
        if ( FloatFrames != null )
        {
            var samplesToDiscard = count * ChannelCount;
            var samplesToKeep    = framesToKeep * ChannelCount;
            Array.Copy(FloatFrames, samplesToDiscard, FloatFrames, 0, samplesToKeep);
        }

        if ( ByteFrames != null )
        {
            var bytesToDiscard = count * FrameSize;
            var bytesToKeep    = framesToKeep * FrameSize;
            Array.Copy(ByteFrames, bytesToDiscard, ByteFrames, 0, bytesToKeep);
        }

        framesDiscarded += count;
    }

    /// <inheritdoc />
    public override Task<Memory<float>> GetSamplesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(startFrame, framesDiscarded, nameof(startFrame));

        return base.GetSamplesAsync(startFrame - framesDiscarded, maxFrames, cancellationToken);
    }

    /// <inheritdoc />
    public override Task<Memory<byte>> GetFramesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(startFrame, framesDiscarded, nameof(startFrame));

        return base.GetFramesAsync(startFrame - framesDiscarded, maxFrames, cancellationToken);
    }

    public override Task<int> CopyFramesAsync(Memory<byte> destination, long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(startFrame, framesDiscarded, nameof(startFrame));

        return base.CopyFramesAsync(destination, startFrame - framesDiscarded, maxFrames, cancellationToken);
    }
}

using PersonaEngine.Lib.Utils;

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Similar with <seealso cref="AwaitableAudioSource" /> allows writing data from a wave file instead of writing the
///     samples directly.
/// </summary>
/// <remarks>
///     Important: This should be used at most by one writer and one reader.
///     It can store samples as floats or as bytes, or both. By default, it stores samples as floats.
///     Based on your usage, you can choose to store samples as bytes, floats, or both.
///     If storing them as floats, they will be deserialized from bytes when they are added and returned directly when
///     requested.
///     If storing them as bytes, they will be serialized from floats when they are added and returned directly when
///     requested.
///     If you want to optimize your memory usage, you can store them in the same format as they are added.
///     If you want to optimize your CPU usage, you can store them in the format you want to use them in.
/// </remarks>
public class AwaitableWaveFileSource(
    IReadOnlyDictionary<string, string> metadata,
    bool                                storeSamples        = true,
    bool                                storeBytes          = false,
    int                                 initialSizeFloats   = BufferedMemoryAudioSource.DefaultInitialSize,
    int                                 initialSizeBytes    = BufferedMemoryAudioSource.DefaultInitialSize,
    IChannelAggregationStrategy?        aggregationStrategy = null)
    : AwaitableAudioSource(metadata, storeSamples, storeBytes, initialSizeFloats, initialSizeBytes, aggregationStrategy)
{
    private MergedMemoryChunks? headerChunks;

    private MergedMemoryChunks? sampleDataChunks;

    public void WriteData(ReadOnlyMemory<byte> data)
    {
        if ( IsFlushed )
        {
            throw new InvalidOperationException("Cannot write to flushed stream.");
        }

        // We need the dataOffset in case the data contains both header and samples
        var dataOffset = 0;
        if ( !IsInitialized )
        {
            if ( headerChunks == null )
            {
                headerChunks = new MergedMemoryChunks(data);
            }
            else
            {
                headerChunks.AddChunk(data);
            }

            var headerParseResult = WaveFileUtils.ParseHeader(headerChunks);
            if ( headerParseResult.IsIncomplete )
            {
                // Need more data for header
                // We need to copy the last chunk as we'll keep it for the next iteration
                headerChunks = new MergedMemoryChunks(headerChunks.ToArray());

                return;
            }

            if ( headerParseResult.IsCorrupt )
            {
                throw new InvalidOperationException(headerParseResult.ErrorMessage);
            }

            if ( !headerParseResult.IsSuccess || headerParseResult.Header == null )
            {
                throw new NotSupportedException(headerParseResult.ErrorMessage);
            }

            base.Initialize(headerParseResult.Header);
            dataOffset = headerParseResult.DataOffset;
        }

        // Now process sample data
        var sampleData = dataOffset == 0 ? data : data.Slice(dataOffset);
        lock (syncRoot)
        {
            ProcessSamples(sampleData);
        }

        NotifyNewSamples();
    }

    private void ProcessSamples(ReadOnlyMemory<byte> sampleData)
    {
        if ( sampleDataChunks != null )
        {
            sampleDataChunks.AddChunk(sampleData);
        }
        else
        {
            sampleDataChunks = new MergedMemoryChunks(sampleData);
        }

        // Calculate how many complete frames we can process
        var framesToProcess = sampleDataChunks.Length / FrameSize;

        if ( framesToProcess == 0 )
        {
            // Not enough data to process even a single frame, wait for more data
            return;
        }

        for ( var frameIndex = 0; frameIndex < framesToProcess; frameIndex++ )
        {
            var frame = sampleDataChunks.GetChunk(FrameSize);
            AddFrame(frame);
        }

        // After processing, check if there are remaining bytes and store them for the next iteration
        // We need to copy the memory (with ToArray()) as otherwise, it might be overriden by the new chunk.
        sampleDataChunks = sampleDataChunks.Length > sampleDataChunks.Position
                               ? new MergedMemoryChunks(sampleDataChunks.GetChunk((int)(sampleDataChunks.Length - sampleDataChunks.Position)).ToArray().AsMemory())
                               : null;
    }

    protected override void Dispose(bool disposing)
    {
        headerChunks     = null;
        sampleDataChunks = null;
        base.Dispose(disposing);
    }
}

using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

/// <summary>
///     パラメータに適用する表情の値を持たせる構造体
/// </summary>
public record ExpressionParameterValue
{
    /// <summary>
    ///     加算値
    /// </summary>
    public float AdditiveValue;

    /// <summary>
    ///     乗算値
    /// </summary>
    public float MultiplyValue;

    /// <summary>
    ///     上書き値
    /// </summary>
    public float OverwriteValue;

    /// <summary>
    ///     パラメータID
    /// </summary>
    public string ParameterId;
}

public class CubismExpressionMotionManager : CubismMotionQueueManager
{
    // モデルに適用する各パラメータの値
    private readonly List<ExpressionParameterValue> _expressionParameterValues = [];

    // 再生中の表情のウェイト
    private readonly List<float> _fadeWeights = [];

    /// <summary>
    ///     現在再生中のモーションの優先度
    /// </summary>
    public MotionPriority CurrentPriority { get; private set; }

    /// <summary>
    ///     再生予定のモーションの優先度。再生中は0になる。モーションファイルを別スレッドで読み込むときの機能。
    /// </summary>
    public MotionPriority ReservePriority { get; set; }

    /// <summary>
    ///     優先度を設定して表情モーションを開始する。
    /// </summary>
    /// <param name="motion">モーション</param>
    /// <param name="priority">優先度</param>
    /// <returns>開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」</returns>
    public CubismMotionQueueEntry StartMotionPriority(ACubismMotion motion, MotionPriority priority)
    {
        if ( priority == ReservePriority )
        {
            ReservePriority = 0; // 予約を解除
        }

        CurrentPriority = priority; // 再生中モーションの優先度を設定

        _fadeWeights.Add(0.0f);

        return StartMotion(motion, UserTimeSeconds);
    }

    /// <summary>
    ///     表情モーションを更新して、モデルにパラメータ値を反映する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="deltaTimeSeconds"> デルタ時間[秒]</param>
    /// <returns>
    ///     true    更新されている
    ///     false   更新されていない
    /// </returns>
    public bool UpdateMotion(CubismModel model, float deltaTimeSeconds)
    {
        UserTimeSeconds += deltaTimeSeconds;
        var updated = false;
        var motions = Motions;

        var expressionWeight = 0.0f;
        var expressionIndex  = 0;

        // If there is already a motion, set the end flag
        var list = new List<CubismMotionQueueEntry>();
        foreach ( var item in motions )
        {
            if ( item.Motion is not CubismExpressionMotion expressionMotion )
            {
                list.Add(item);

                continue;
            }

            var expressionParameters = expressionMotion.Parameters;
            if ( item.Available )
            {
                // 再生中のExpressionが参照しているパラメータをすべてリストアップ
                for ( var i = 0; i < expressionParameters.Count; ++i )
                {
                    if ( expressionParameters[i].ParameterId == null )
                    {
                        continue;
                    }

                    var index = -1;
                    // リストにパラメータIDが存在するか検索
                    for ( var j = 0; j < _expressionParameterValues.Count; ++j )
                    {
                        if ( _expressionParameterValues[j].ParameterId != expressionParameters[i].ParameterId )
                        {
                            continue;
                        }

                        index = j;

                        break;
                    }

                    if ( index >= 0 )
                    {
                        continue;
                    }

                    // If the parameter does not exist in the list, add it.
                    ExpressionParameterValue item1 = new() { ParameterId = expressionParameters[i].ParameterId, AdditiveValue = CubismExpressionMotion.DefaultAdditiveValue, MultiplyValue = CubismExpressionMotion.DefaultMultiplyValue };
                    item1.OverwriteValue = model.GetParameterValue(item1.ParameterId);
                    _expressionParameterValues.Add(item1);
                }
            }

            // ------ 値を計算する ------
            expressionMotion.SetupMotionQueueEntry(item, UserTimeSeconds);
            _fadeWeights[expressionIndex] = expressionMotion.UpdateFadeWeight(item, UserTimeSeconds);
            expressionMotion.CalculateExpressionParameters(model, UserTimeSeconds,
                                                           item, _expressionParameterValues, expressionIndex, _fadeWeights[expressionIndex]);

            expressionWeight += expressionMotion.FadeInSeconds == 0.0f
                                    ? 1.0f
                                    : CubismMath.GetEasingSine((UserTimeSeconds - item.FadeInStartTime) / expressionMotion.FadeInSeconds);

            updated = true;

            if ( item.IsTriggeredFadeOut )
            {
                // フェードアウト開始
                item.StartFadeout(item.FadeOutSeconds, UserTimeSeconds);
            }

            ++expressionIndex;
        }

        // ----- 最新のExpressionのフェードが完了していればそれ以前を削除する ------
        if ( motions.Count > 1 )
        {
            var latestFadeWeight = _fadeWeights[_fadeWeights.Count - 1];
            if ( latestFadeWeight >= 1.0f )
            {
                // 配列の最後の要素は削除しない
                for ( var i = motions.Count - 2; i >= 0; i-- )
                {
                    motions.RemoveAt(i);
                    _fadeWeights.RemoveAt(i);
                }
            }
        }

        if ( expressionWeight > 1.0f )
        {
            expressionWeight = 1.0f;
        }

        // モデルに各値を適用
        for ( var i = 0; i < _expressionParameterValues.Count; ++i )
        {
            model.SetParameterValue(_expressionParameterValues[i].ParameterId,
                                    (_expressionParameterValues[i].OverwriteValue + _expressionParameterValues[i].AdditiveValue) * _expressionParameterValues[i].MultiplyValue,
                                    expressionWeight);

            _expressionParameterValues[i].AdditiveValue = CubismExpressionMotion.DefaultAdditiveValue;
            _expressionParameterValues[i].MultiplyValue = CubismExpressionMotion.DefaultMultiplyValue;
        }

        return updated;
    }

    /// <summary>
    ///     現在の表情のフェードのウェイト値を取得する。
    /// </summary>
    /// <param name="index">取得する表情モーションのインデックス</param>
    /// <returns>表情のフェードのウェイト値</returns>
    public float GetFadeWeight(int index) { return _fadeWeights[index]; }
}

using Microsoft.ML.OnnxRuntime.Tensors;

namespace PersonaEngine.Lib.TTS.RVC;

internal static class RVCUtils
{
    public static DenseTensor<float> RepeatTensor(DenseTensor<float> tensor, int times)
    {
        // Create output tensor with expanded dimensions
        var inputDims  = tensor.Dimensions;
        var outputDims = inputDims.ToArray();
        outputDims[2] *= times;
        var result = new DenseTensor<float>(outputDims);

        // Cache dimensions for faster access
        var dim0    = inputDims[0];
        var dim1    = inputDims[1];
        var dim2    = inputDims[2];
        var newDim2 = dim2 * times;

        // Compute strides for efficient indexing
        var inputStride1 = dim2;
        var inputStride0 = dim1 * inputStride1;

        var outputStride1 = newDim2;
        var outputStride0 = dim1 * outputStride1;

        // Use regular for-loop with thread partitioning instead of Parallel.For with lambda
        var totalBatches     = dim0;
        var numThreads       = Environment.ProcessorCount;
        var batchesPerThread = (totalBatches + numThreads - 1) / numThreads;

        Parallel.For(0, numThreads, threadIndex =>
                                    {
                                        // Calculate the range for this thread
                                        var startBatch = threadIndex * batchesPerThread;
                                        var endBatch   = Math.Min(startBatch + batchesPerThread, totalBatches);

                                        // Process assigned batches without capturing Span
                                        for ( var i = startBatch; i < endBatch; i++ )
                                        {
                                            // Calculate base offsets for each dimension
                                            var inputBaseI  = i * inputStride0;
                                            var outputBaseI = i * outputStride0;

                                            for ( var j = 0; j < dim1; j++ )
                                            {
                                                var inputBaseJ  = inputBaseI + j * inputStride1;
                                                var outputBaseJ = outputBaseI + j * outputStride1;

                                                for ( var k = 0; k < dim2; k++ )
                                                {
                                                    // Get input value using direct indexers
                                                    var value          = tensor.GetValue(inputBaseJ + k);
                                                    var outputStartIdx = outputBaseJ + k * times;

                                                    // Set output values in a loop
                                                    for ( var t = 0; t < times; t++ )
                                                    {
                                                        result.SetValue(outputStartIdx + t, value);
                                                    }
                                                }
                                            }
                                        }
                                    });

        return result;
    }

    public static DenseTensor<T> Transpose<T>(Tensor<T> tensor, params int[] perm)
    {
        // Create output tensor with transposed dimensions
        var outputDims = new int[perm.Length];
        for ( var i = 0; i < perm.Length; i++ )
        {
            outputDims[i] = tensor.Dimensions[perm[i]];
        }

        var result = new DenseTensor<T>(outputDims);

        // Optimize for the specific (0,2,1) permutation used in the codebase
        if ( tensor.Dimensions.Length == 3 && perm.Length == 3 &&
             perm[0] == 0 && perm[1] == 2 && perm[2] == 1 )
        {
            TransposeOptimized021(tensor, result);

            return result;
        }

        var rank = tensor.Dimensions.Length;

        // Precompute input strides for faster coordinate calculation
        var inputStrides = new int[rank];
        inputStrides[rank - 1] = 1;
        for ( var i = rank - 2; i >= 0; i-- )
        {
            inputStrides[i] = inputStrides[i + 1] * tensor.Dimensions[i + 1];
        }

        // Precompute output strides
        var outputStrides = new int[rank];
        outputStrides[rank - 1] = 1;
        for ( var i = rank - 2; i >= 0; i-- )
        {
            outputStrides[i] = outputStrides[i + 1] * outputDims[i + 1];
        }

        // Process in chunks for better parallelization
        const int chunkSize   = 4096;
        var       totalChunks = (tensor.Length + chunkSize - 1) / chunkSize;

        Parallel.For(0, totalChunks, chunkIdx =>
                                     {
                                         var startIdx = (int)chunkIdx * chunkSize;
                                         var endIdx   = Math.Min(startIdx + chunkSize, tensor.Length);

                                         // Allocate coordinate array for this thread
                                         var coords = new int[rank];

                                         // Process this chunk
                                         for ( var flatIdx = startIdx; flatIdx < endIdx; flatIdx++ )
                                         {
                                             // Convert flat index to coordinates
                                             var remaining = flatIdx;
                                             for ( var dim = 0; dim < rank; dim++ )
                                             {
                                                 coords[dim] =  remaining / inputStrides[dim];
                                                 remaining   %= inputStrides[dim];
                                             }

                                             // Compute output index
                                             var outputIdx = 0;
                                             for ( var dim = 0; dim < rank; dim++ )
                                             {
                                                 outputIdx += coords[perm[dim]] * outputStrides[dim];
                                             }

                                             // Transfer the value
                                             result.SetValue(outputIdx, tensor.GetValue(flatIdx));
                                         }
                                     });

        return result;
    }

    // Highly optimized method for the specific (0,2,1) permutation
    private static void TransposeOptimized021<T>(Tensor<T> input, DenseTensor<T> output)
    {
        var dim0 = input.Dimensions[0];
        var dim1 = input.Dimensions[1];
        var dim2 = input.Dimensions[2];

        // Partitioning for parallel processing
        var numThreads       = Environment.ProcessorCount;
        var batchesPerThread = (dim0 + numThreads - 1) / numThreads;

        Parallel.For(0, numThreads, threadIndex =>
                                    {
                                        var startBatch = threadIndex * batchesPerThread;
                                        var endBatch   = Math.Min(startBatch + batchesPerThread, dim0);

                                        // Process batches assigned to this thread
                                        for ( var i = startBatch; i < endBatch; i++ )
                                        {
                                            // Compute base offsets for this batch
                                            var inputBatchOffset  = i * dim1 * dim2;
                                            var outputBatchOffset = i * dim2 * dim1;

                                            // Cache-friendly block processing
                                            const int blockSize = 16; // Tuned for typical CPU cache line size

                                            // Process the 2D slice in blocks for better cache locality
                                            for ( var jBlock = 0; jBlock < dim1; jBlock += blockSize )
                                            {
                                                for ( var kBlock = 0; kBlock < dim2; kBlock += blockSize )
                                                {
                                                    // Determine actual block boundaries
                                                    var jEnd = Math.Min(jBlock + blockSize, dim1);
                                                    var kEnd = Math.Min(kBlock + blockSize, dim2);

                                                    // Process the block
                                                    for ( var j = jBlock; j < jEnd; j++ )
                                                    {
                                                        var inputRowOffset = inputBatchOffset + j * dim2;

                                                        for ( var k = kBlock; k < kEnd; k++ )
                                                        {
                                                            var inputIdx  = inputRowOffset + k;
                                                            var outputIdx = outputBatchOffset + k * dim1 + j;

                                                            // Use element access methods instead of Span
                                                            output.SetValue(outputIdx, input.GetValue(inputIdx));
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    });
    }
}

using Microsoft.Extensions.Logging;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Implementation of text processing for TTS
/// </summary>
public class TextProcessor : ITextProcessor
{
    private readonly ILogger<TextProcessor> _logger;

    private readonly ITextNormalizer _normalizer;

    private readonly ISentenceSegmenter _segmenter;

    public TextProcessor(
        ITextNormalizer        normalizer,
        ISentenceSegmenter     segmenter,
        ILogger<TextProcessor> logger)
    {
        _normalizer = normalizer ?? throw new ArgumentNullException(nameof(normalizer));
        _segmenter  = segmenter ?? throw new ArgumentNullException(nameof(segmenter));
        _logger     = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>
    ///     Processes text for TTS by normalizing and segmenting into sentences
    /// </summary>
    public Task<ProcessedText> ProcessAsync(string text, CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            _logger.LogInformation("Empty text received for processing");

            return Task.FromResult(new ProcessedText(string.Empty, Array.Empty<string>()));
        }

        cancellationToken.ThrowIfCancellationRequested();

        try
        {
            // Normalize the text
            var normalizedText = _normalizer.Normalize(text);

            if ( string.IsNullOrEmpty(normalizedText) )
            {
                _logger.LogWarning("Text normalization resulted in empty text");

                return Task.FromResult(new ProcessedText(string.Empty, Array.Empty<string>()));
            }

            // Segment into sentences
            var sentences = _segmenter.Segment(normalizedText);

            _logger.LogDebug("Processed text into {SentenceCount} sentences", sentences.Count);

            return Task.FromResult(new ProcessedText(normalizedText, sentences));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing text");

            throw;
        }
    }
}

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;

using Stateless;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Session;

public partial class ConversationSession
{
    private readonly StateMachine<ConversationState, ConversationTrigger> _stateMachine;

    private StateMachine<ConversationState, ConversationTrigger>.TriggerWithParameters<IInputEvent> _inputDetectedTrigger = null!;

    private StateMachine<ConversationState, ConversationTrigger>.TriggerWithParameters<IInputEvent> _inputFinalizedTrigger = null!;

    private void ConfigureStateMachine()
    {
        _inputDetectedTrigger  = _stateMachine.SetTriggerParameters<IInputEvent>(ConversationTrigger.InputDetected);
        _inputFinalizedTrigger = _stateMachine.SetTriggerParameters<IInputEvent>(ConversationTrigger.InputFinalized);

        var llmChunkReceivedTrigger = _stateMachine.SetTriggerParameters<IOutputEvent>(ConversationTrigger.LlmStreamChunkReceived);
        var ttsChunkReceivedTrigger = _stateMachine.SetTriggerParameters<IOutputEvent>(ConversationTrigger.TtsStreamChunkReceived);
        var errorOccurredTrigger    = _stateMachine.SetTriggerParameters<Exception>(ConversationTrigger.ErrorOccurred);

        _stateMachine.Configure(ConversationState.Initial)
                     .Permit(ConversationTrigger.InitializeRequested, ConversationState.Initializing)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error);

        _stateMachine.Configure(ConversationState.Initializing)
                     .OnEntryAsync(InitializeSessionAsync, "Initialize Session Resources")
                     .Permit(ConversationTrigger.InitializeComplete, ConversationState.Idle)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended);

        _stateMachine.Configure(ConversationState.Idle)
                     .OnEntry(HandleIdle, "Handle Idle State")
                     .Ignore(ConversationTrigger.LlmStreamEnded)
                     .Ignore(ConversationTrigger.TtsStreamEnded)
                     .Ignore(ConversationTrigger.AudioStreamEnded)
                     .Permit(ConversationTrigger.InputDetected, ConversationState.Listening)
                     .Permit(ConversationTrigger.InputFinalized, ConversationState.ProcessingInput)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error);

        _stateMachine.Configure(ConversationState.Listening)
                     .SubstateOf(ConversationState.Idle)
                     .Permit(ConversationTrigger.InputFinalized, ConversationState.ProcessingInput)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.PauseRequested, ConversationState.Paused)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error)
                     .Ignore(ConversationTrigger.InputDetected);

        _stateMachine.Configure(ConversationState.ActiveTurn)
                     .PermitIf(_inputDetectedTrigger, ConversationState.Interrupted,
                               ShouldAllowBargeIn, "Barge-In")
                     .PermitIf(_inputFinalizedTrigger, ConversationState.Interrupted,
                               ShouldAllowBargeIn, "Barge-In");

        _stateMachine.Configure(ConversationState.ProcessingInput)
                     .SubstateOf(ConversationState.ActiveTurn)
                     .OnEntryFromAsync(_inputFinalizedTrigger, PrepareLlmRequestAsync, "Process Input and Call LLM")
                     .Ignore(ConversationTrigger.LlmStreamEnded)
                     .Ignore(ConversationTrigger.TtsStreamEnded)
                     .Ignore(ConversationTrigger.AudioStreamEnded)
                     .Permit(ConversationTrigger.LlmRequestSent, ConversationState.WaitingForLlm)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error);

        _stateMachine.Configure(ConversationState.WaitingForLlm)
                     .SubstateOf(ConversationState.ActiveTurn)
                     .OnEntryFrom(ConversationTrigger.LlmRequestSent, HandleLlmStreamRequested, "Start LLM")
                     .InternalTransition(ConversationTrigger.TtsRequestSent, HandleTtsStreamRequest)
                     .Permit(ConversationTrigger.LlmStreamStarted, ConversationState.StreamingResponse)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error);

        _stateMachine.Configure(ConversationState.StreamingResponse)
                     .SubstateOf(ConversationState.ActiveTurn)
                     .InternalTransitionAsync(llmChunkReceivedTrigger, HandleLlmStreamChunkReceived)
                     .InternalTransitionAsync(ttsChunkReceivedTrigger, HandleTtsStreamChunkReceived)
                     .InternalTransition(ConversationTrigger.TtsStreamEnded, HandleTtsStreamEnded)
                     .InternalTransition(ConversationTrigger.LlmStreamEnded, HandleLlmStreamEnded)
                     .Ignore(ConversationTrigger.TtsStreamStarted)
                     .Permit(ConversationTrigger.AudioStreamStarted, ConversationState.Speaking)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error);

        _stateMachine.Configure(ConversationState.Speaking)
                     .SubstateOf(ConversationState.ActiveTurn)
                     .OnExit(CommitSpokenText)
                     .InternalTransitionAsync(llmChunkReceivedTrigger, HandleLlmStreamChunkReceived)
                     .InternalTransitionAsync(ttsChunkReceivedTrigger, HandleTtsStreamChunkReceived)
                     .InternalTransition(ConversationTrigger.TtsStreamStarted, HandleTtsStreamRequest)
                     .InternalTransition(ConversationTrigger.TtsStreamEnded, HandleTtsStreamEnded)
                     .InternalTransition(ConversationTrigger.LlmStreamEnded, HandleLlmStreamEnded)
                     .Permit(ConversationTrigger.AudioStreamEnded, ConversationState.Idle)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error);

        _stateMachine.Configure(ConversationState.Interrupted)
                     .OnEntryFromAsync(_inputDetectedTrigger, HandleInterruptionCancelAndRefireAsync, "Handle Detected Interruption, Cancel, Refire")
                     .OnEntryFromAsync(_inputFinalizedTrigger, HandleInterruptionCancelAndRefireAsync, "Handle Finalized Interruption, Cancel, Refire")
                     .OnExitAsync(CancelCurrentTurnProcessingAsync)
                     .Ignore(ConversationTrigger.TtsRequestSent)
                     .Ignore(ConversationTrigger.LlmStreamChunkReceived)
                     .Ignore(ConversationTrigger.TtsStreamChunkReceived)
                     .Ignore(ConversationTrigger.LlmStreamStarted)
                     .Ignore(ConversationTrigger.TtsStreamStarted)
                     .Ignore(ConversationTrigger.AudioStreamStarted)
                     .Ignore(ConversationTrigger.LlmStreamEnded)
                     .Ignore(ConversationTrigger.TtsStreamEnded)
                     .Ignore(ConversationTrigger.AudioStreamEnded)
                     .Permit(ConversationTrigger.InputDetected, ConversationState.Listening)
                     .Permit(ConversationTrigger.InputFinalized, ConversationState.ProcessingInput)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error);

        _stateMachine.Configure(ConversationState.Paused)
                     .OnEntryAsync(PauseActivitiesAsync, "Pause Adapters/Activities")
                     .Permit(ConversationTrigger.ResumeRequested, ConversationState.Idle)
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Permit(ConversationTrigger.ErrorOccurred, ConversationState.Error)
                     .OnExitAsync(ResumeActivitiesAsync, "Resume Adapters/Activities");

        _stateMachine.Configure(ConversationState.Error)
                     .OnEntryFromAsync(errorOccurredTrigger, HandleErrorAsync, "Log Error and Update Context")
                     .OnEntryAsync(CancelCurrentTurnProcessingAsync, "Ensure Pipeline Cancelled on Error")
                     .Permit(ConversationTrigger.StopRequested, ConversationState.Ended)
                     .Ignore(ConversationTrigger.ErrorOccurred);

        _stateMachine.Configure(ConversationState.Ended)
                     .OnEntryAsync(CancelCurrentTurnProcessingAsync, "Ensure Pipeline Cancelled on End")
                     .OnEntryAsync(CleanupSessionAsync, "Stop Adapters and Cleanup Resources");

        _stateMachine.OnUnhandledTriggerAsync(async (state, trigger, unmetGuards) =>
                                              {
                                                  if ( _stateMachine.IsInState(ConversationState.Ended) || _stateMachine.IsInState(ConversationState.Error) )
                                                  {
                                                      return;
                                                  }

                                                  if ( unmetGuards.Count != 0 && unmetGuards.First() == "Barge-In" )
                                                  {
                                                      return;
                                                  }

                                                  _logger.LogWarning("{SessionId} | Unhandled trigger '{Trigger}' in state '{State}'. Unmet guards: [{UnmetGuards}]",
                                                                     SessionId, trigger, state, string.Join(", ", unmetGuards ?? Array.Empty<string>()));

                                                  await _stateMachine.FireAsync(ConversationTrigger.ErrorOccurred, new InvalidOperationException($"Unhandled trigger {trigger} in state {state}"));
                                              });

        _stateMachine.OnTransitionedAsync(async transition =>
                                          {
                                              _logger.LogDebug("{SessionId} | Transitioned: {SourceState} --({Trigger})--> {DestinationState}",
                                                               SessionId, transition.Source, transition.Trigger, transition.Destination);

                                              await ValueTask.CompletedTask;
                                          });

        _stateMachine.OnTransitioned(LogState);
    }

    private void LogState(StateMachine<ConversationState, ConversationTrigger>.Transition transition)
    {
        if ( transition.Source == transition.Destination )
        {
            return;
        }

        var emoji = transition.Destination switch {
            ConversationState.Initializing => "🤖",
            ConversationState.Idle => "🥱",
            ConversationState.Listening => "🧐",
            ConversationState.ProcessingInput => "🤔",
            ConversationState.WaitingForLlm => "😑",
            ConversationState.StreamingResponse => "😃",
            ConversationState.Speaking => "🗣",
            ConversationState.Interrupted => "😵",
            ConversationState.Paused => "🤐",
            ConversationState.Error => "😱",
            ConversationState.Ended => "🙃",
            _ => string.Empty
        };

        _logger.LogInformation("{SessionId} | {TurnId} | {Emoji} | {DestinationState}",
                               SessionId, _currentTurnId ?? Guid.Empty, emoji, transition.Destination);
    }

    private bool ShouldAllowBargeIn(IInputEvent inputEvent)
    {
        var context = new BargeInContext(
                                         _options,
                                         _stateMachine.State,
                                         inputEvent,
                                         SessionId,
                                         _currentTurnId
                                        );

        var allow = _bargeInStrategy.ShouldAllowBargeIn(context);

        _logger.LogDebug("{SessionId} | Barge-in check for trigger {TriggerType} in state {State}. Strategy decided: {Allow}",
                         SessionId, inputEvent.GetType().Name, context.CurrentState, allow);

        return allow;
    }
}

using System.Diagnostics;
using System.Diagnostics.Metrics;

using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Metrics;

public sealed class ConversationMetrics : IDisposable
{
    public const string MeterName = "PersonaEngine.Conversation";

    private readonly Histogram<double> _audioPlaybackDurationMs;

    private readonly Histogram<double> _endToEndTurnDurationMs;

    private readonly Counter<long> _errorsCounter;

    private readonly Histogram<double> _firstAudioLatencyMs;

    private readonly Histogram<double> _llmResponseDurationMs;

    private readonly Meter _meter;

    private readonly Histogram<double> _sttSegmentDurationMs;

    private readonly Histogram<double> _ttsSynthesisDurationMs;

    private readonly Counter<long> _turnsInterruptedCounter;

    private readonly Counter<long> _turnsStartedCounter;

    public ConversationMetrics(IMeterFactory meterFactory)
    {
        _meter = meterFactory.Create(MeterName);

        _endToEndTurnDurationMs = _meter.CreateHistogram<double>(
                                                                 "personaengine.conversation.turn.duration",
                                                                 "ms",
                                                                 "End-to-end duration of a conversation turn (Input Finalized to Audio End).");

        _llmResponseDurationMs = _meter.CreateHistogram<double>(
                                                                "personaengine.conversation.llm.duration",
                                                                "ms",
                                                                "Duration of LLM response generation (Request Sent to Stream End).");

        _ttsSynthesisDurationMs = _meter.CreateHistogram<double>(
                                                                 "personaengine.conversation.tts.duration",
                                                                 "ms",
                                                                 "Duration of TTS synthesis (First Input Chunk to Stream End).");

        _firstAudioLatencyMs = _meter.CreateHistogram<double>(
                                                              "personaengine.conversation.turn.first_audio_latency",
                                                              "ms",
                                                              "Latency from user input finalization to the start of the assistant's audio playback.");

        _audioPlaybackDurationMs = _meter.CreateHistogram<double>(
                                                                  "personaengine.conversation.audio.playback.duration",
                                                                  "ms",
                                                                  "Duration of audio playback for the assistant's response.");

        _sttSegmentDurationMs = _meter.CreateHistogram<double>(
                                                               "personaengine.conversation.stt.segment.duration",
                                                               "ms",
                                                               "Duration of STT processing for a single recognized segment.");

        _turnsStartedCounter = _meter.CreateCounter<long>(
                                                          "personaengine.conversation.turns.started.count",
                                                          "{turn}",
                                                          "Number of conversation turns started.");

        _turnsInterruptedCounter = _meter.CreateCounter<long>(
                                                              "personaengine.conversation.turns.interrupted.count",
                                                              "{turn}",
                                                              "Number of conversation turns interrupted by barge-in.");

        _errorsCounter = _meter.CreateCounter<long>(
                                                    "personaengine.conversation.errors.count",
                                                    "{error}",
                                                    "Number of errors encountered during conversation processing.");
    }

    public void Dispose() { _meter.Dispose(); }

    public void RecordTurnDuration(double durationMs, Guid sessionId, Guid turnId, CompletionReason reason)
    {
        if ( !_endToEndTurnDurationMs.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "TurnId", turnId.ToString() }, { "FinishReason", reason.ToString() } };

        _endToEndTurnDurationMs.Record(durationMs, tags);
    }

    public void RecordLlmDuration(double durationMs, Guid sessionId, Guid turnId, CompletionReason reason)
    {
        if ( !_llmResponseDurationMs.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "TurnId", turnId.ToString() }, { "FinishReason", reason.ToString() } };

        _llmResponseDurationMs.Record(durationMs, tags);
    }

    public void RecordTtsDuration(double durationMs, Guid sessionId, Guid turnId, CompletionReason reason)
    {
        if ( !_ttsSynthesisDurationMs.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "TurnId", turnId.ToString() }, { "FinishReason", reason.ToString() } };

        _ttsSynthesisDurationMs.Record(durationMs, tags);
    }

    public void RecordAudioPlaybackDuration(double durationMs, Guid sessionId, Guid turnId, CompletionReason reason)
    {
        if ( !_audioPlaybackDurationMs.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "TurnId", turnId.ToString() }, { "FinishReason", reason.ToString() } };

        _audioPlaybackDurationMs.Record(durationMs, tags);
    }

    public void RecordSttSegmentDuration(double durationMs, Guid sessionId, string participantId)
    {
        if ( !_sttSegmentDurationMs.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "ParticipantId", participantId } };

        _sttSegmentDurationMs.Record(durationMs, tags);
    }

    public void IncrementTurnsStarted(Guid sessionId)
    {
        if ( !_turnsStartedCounter.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() } };
        _turnsStartedCounter.Add(1, tags);
    }

    public void IncrementTurnsInterrupted(Guid sessionId, Guid turnId)
    {
        if ( !_turnsInterruptedCounter.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "TurnId", turnId.ToString() } };
        _turnsInterruptedCounter.Add(1, tags);
    }

    public void IncrementErrors(Guid sessionId, Guid? turnId, Exception exception)
    {
        if ( !_errorsCounter.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "ErrorType", exception.GetType().Name } };

        if ( turnId.HasValue )
        {
            tags.Add(new KeyValuePair<string, object?>("TurnId", turnId.Value.ToString()));
        }

        _errorsCounter.Add(1, tags);
    }

    public void RecordFirstAudioLatency(double durationMs, Guid sessionId, Guid turnId)
    {
        if ( !_firstAudioLatencyMs.Enabled )
        {
            return;
        }

        var tags = new TagList { { "SessionId", sessionId.ToString() }, { "TurnId", turnId.ToString() } };

        _firstAudioLatencyMs.Record(durationMs, tags);
    }
}

using System.Numerics;

using Hexa.NET.ImGui;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Static utility class for common UI styling operations
/// </summary>
public static class UiStyler
{
    /// <summary>
    ///     Executes an action with a temporary style color change
    /// </summary>
    public static void WithStyleColor(ImGuiCol colorId, Vector4 color, Action action)
    {
        ImGui.PushStyleColor(colorId, color);
        try
        {
            action();
        }
        finally
        {
            ImGui.PopStyleColor();
        }
    }

    /// <summary>
    ///     Executes an action with multiple temporary style color changes
    /// </summary>
    public static void WithStyleColors(IReadOnlyList<(ImGuiCol, Vector4)> colors, Action action)
    {
        foreach ( var (colorId, color) in colors )
        {
            ImGui.PushStyleColor(colorId, color);
        }

        try
        {
            action();
        }
        finally
        {
            ImGui.PopStyleColor(colors.Count);
        }
    }

    /// <summary>
    ///     Executes an action with a temporary style variable change
    /// </summary>
    public static void WithStyleVar(ImGuiStyleVar styleVar, Vector2 value, Action action)
    {
        ImGui.PushStyleVar(styleVar, value);
        try
        {
            action();
        }
        finally
        {
            ImGui.PopStyleVar();
        }
    }

    /// <summary>
    ///     Executes an action with a temporary style variable change
    /// </summary>
    public static void WithStyleVar(ImGuiStyleVar styleVar, float value, Action action)
    {
        ImGui.PushStyleVar(styleVar, value);
        try
        {
            action();
        }
        finally
        {
            ImGui.PopStyleVar();
        }
    }

    /// <summary>
    ///     Displays a help marker with a tooltip
    /// </summary>
    public static void HelpMarker(string desc)
    {
        ImGui.TextDisabled("(?)");
        if ( ImGui.IsItemHovered(ImGuiHoveredFlags.DelayShort) )
        {
            ImGui.BeginTooltip();
            ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f);
            ImGui.TextUnformatted(desc);
            ImGui.PopTextWrapPos();
            ImGui.EndTooltip();
        }
    }

    /// <summary>
    ///     Renders an input text field with optional validation
    /// </summary>
    public static bool ValidatedInputText(
        string              label,
        ref string          value,
        uint                bufferSize,
        string?             tooltip   = null,
        Func<string, bool>? validator = null)
    {
        var changed = ImGui.InputText(label, ref value, bufferSize);

        // Show tooltip if provided
        if ( tooltip != null && ImGui.IsItemHovered(ImGuiHoveredFlags.DelayShort) )
        {
            ImGui.BeginTooltip();
            ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f);
            ImGui.TextUnformatted(tooltip);
            ImGui.PopTextWrapPos();
            ImGui.EndTooltip();
        }

        // Validate if changed and validator provided
        if ( changed && validator != null )
        {
            var isValid = validator(value);
            if ( !isValid )
            {
                ImGui.SameLine();
                ImGui.TextColored(new Vector4(1, 0, 0, 1), "!");
                if ( ImGui.IsItemHovered(ImGuiHoveredFlags.DelayShort) )
                {
                    ImGui.BeginTooltip();
                    ImGui.TextUnformatted("Invalid input");
                    ImGui.EndTooltip();
                }
            }
        }

        return changed;
    }

    /// <summary>
    ///     Renders an input integer field with optional validation and constraints
    /// </summary>
    public static bool ValidatedInputInt(
        string           label,
        ref int          value,
        string?          tooltip   = null,
        Func<int, bool>? validator = null,
        int?             min       = null,
        int?             max       = null)
    {
        var changed = ImGui.InputInt(label, ref value);

        // Apply min/max constraints
        if ( changed )
        {
            if ( min.HasValue && value < min.Value )
            {
                value = min.Value;
            }

            if ( max.HasValue && value > max.Value )
            {
                value = max.Value;
            }
        }

        // Show tooltip if provided
        if ( tooltip != null && ImGui.IsItemHovered(ImGuiHoveredFlags.DelayShort) )
        {
            ImGui.BeginTooltip();
            ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f);
            ImGui.TextUnformatted(tooltip);
            ImGui.PopTextWrapPos();
            ImGui.EndTooltip();
        }

        // Validate if changed and validator provided
        if ( changed && validator != null )
        {
            var isValid = validator(value);
            if ( !isValid )
            {
                ImGui.SameLine();
                ImGui.TextColored(new Vector4(1, 0, 0, 1), "!");
                if ( ImGui.IsItemHovered(ImGuiHoveredFlags.DelayShort) )
                {
                    ImGui.BeginTooltip();
                    ImGui.TextUnformatted("Invalid input");
                    ImGui.EndTooltip();
                }
            }
        }

        return changed;
    }

    /// <summary>
    ///     Renders a float slider with tooltip and optional animation
    /// </summary>
    public static bool TooltipSliderFloat(
        string    label,
        ref float value,
        float     min,
        float     max,
        string    format  = "%.3f",
        string?   tooltip = null,
        bool      animate = false)
    {
        if ( animate )
        {
            // For animated sliders, use a pulsing accent color
            var time  = (float)Math.Sin(ImGui.GetTime() * 2.0f) * 0.5f + 0.5f;
            var color = new Vector4(0.1f, 0.4f + time * 0.2f, 0.8f + time * 0.2f, 1.0f);
            ImGui.PushStyleColor(ImGuiCol.FrameBg, new Vector4(0.3f, 0.3f, 0.3f, 1.0f));
            ImGui.PushStyleColor(ImGuiCol.SliderGrab, color);
        }

        var changed = ImGui.SliderFloat(label, ref value, min, max, format);

        if ( animate )
        {
            ImGui.PopStyleColor(2);
        }

        if ( tooltip != null && ImGui.IsItemHovered(ImGuiHoveredFlags.DelayShort) )
        {
            ImGui.BeginTooltip();
            ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f);
            ImGui.TextUnformatted(tooltip);
            ImGui.PopTextWrapPos();
            ImGui.EndTooltip();
        }

        return changed;
    }

    /// <summary>
    ///     Renders a collapsible section header
    /// </summary>
    /// <summary>
    ///     Renders a collapsible section header
    /// </summary>
    public static bool SectionHeader(string label, bool defaultOpen = true)
    {
        // Define the style colors
        var styleColors = new List<(ImGuiCol, Vector4)> { (ImGuiCol.Header, new Vector4(0.15f, 0.45f, 0.8f, 0.8f)), (ImGuiCol.HeaderHovered, new Vector4(0.2f, 0.5f, 0.9f, 0.8f)), (ImGuiCol.HeaderActive, new Vector4(0.25f, 0.55f, 0.95f, 0.8f)) };

        var flags = defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None;
        flags |= ImGuiTreeNodeFlags.Framed;
        flags |= ImGuiTreeNodeFlags.SpanAvailWidth;
        flags |= ImGuiTreeNodeFlags.AllowOverlap;
        flags |= ImGuiTreeNodeFlags.FramePadding;

        // Use the WithStyleColors method properly - pass the actual rendering logic in the action
        var opened = false;
        WithStyleColors(styleColors, () => { WithStyleVar(ImGuiStyleVar.FramePadding, new Vector2(6, 6), () => { opened = ImGui.CollapsingHeader(label, flags); }); });

        return opened;
    }

    /// <summary>
    ///     Renders a button with animation effects
    /// </summary>
    public static bool AnimatedButton(string label, Vector2 size, bool isActive = false)
    {
        bool clicked;
        if ( isActive )
        {
            // Pulse animation for active button
            var time  = (float)Math.Sin(ImGui.GetTime() * 2.0f) * 0.5f + 0.5f;
            var color = new Vector4(0.1f, 0.5f + time * 0.3f, 0.9f, 0.7f + time * 0.3f);
            ImGui.PushStyleColor(ImGuiCol.Button, color);
            ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(color.X, color.Y, color.Z, 1.0f));
            ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(color.X * 1.1f, color.Y * 1.1f, color.Z * 1.1f, 1.0f));
            clicked = ImGui.Button(label, size);
            ImGui.PopStyleColor(3);
        }
        else
        {
            // Default stylish button
            // ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0.15f, 0.45f, 0.8f, 0.6f));
            // ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0.2f, 0.5f, 0.9f, 0.7f));
            // ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.25f, 0.55f, 0.95f, 0.8f));
            clicked = ImGui.Button(label, size);
        }

        return clicked;
    }
}

using System.Globalization;
using System.Runtime.CompilerServices;

using PersonaEngine.Lib.Audio;

using Whisper.net;

namespace PersonaEngine.Lib.ASR.Transcriber;

internal class WhisperNetSpeechTranscriptor(WhisperProcessor whisperProcessor) : ISpeechTranscriptor
{
    public async IAsyncEnumerable<TranscriptSegment> TranscribeAsync(IAudioSource source, [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        if ( source.ChannelCount != 1 )
        {
            throw new NotSupportedException("Only mono-channel audio is supported. Consider one channel aggregation on the audio source.");
        }

        if ( source.SampleRate != 16000 )
        {
            throw new NotSupportedException("Only 16 kHz audio is supported. Consider resampling before calling this transcriptor.");
        }

        var samples = await source.GetSamplesAsync(0, cancellationToken: cancellationToken);

        await foreach ( var segment in whisperProcessor.ProcessAsync(samples, cancellationToken) )
        {
            yield return new TranscriptSegment {
                                                   Metadata        = source.Metadata,
                                                   StartTime       = segment.Start,
                                                   Duration        = segment.End - segment.Start,
                                                   ConfidenceLevel = segment.Probability,
                                                   Language        = new CultureInfo(segment.Language),
                                                   Text            = segment.Text,
                                                   Tokens = segment?.Tokens.Select(t => new TranscriptToken {
                                                                                Id                     = t.Id,
                                                                                Text                   = t.Text,
                                                                                Confidence             = t.Probability,
                                                                                ConfidenceLog          = t.ProbabilityLog,
                                                                                StartTime              = TimeSpan.FromMilliseconds(t.Start),
                                                                                Duration               = TimeSpan.FromMilliseconds(t.End - t.Start),
                                                                                DtwTimestamp           = t.DtwTimestamp,
                                                                                TimestampConfidence    = t.TimestampProbability,
                                                                                TimestampConfidenceSum = t.TimestampProbabilitySum
                                                                                }).ToList()
                                               };
        }
    }

    public async ValueTask DisposeAsync() { await whisperProcessor.DisposeAsync(); }
}

using System.Numerics;

using PersonaEngine.Lib.Live2D.Framework.Core;
using PersonaEngine.Lib.Live2D.Framework.Rendering;

namespace PersonaEngine.Lib.Live2D.Framework.Model;

public class CubismModel : IDisposable
{
    /// <summary>
    ///     存在していないパラメータIDのリスト
    /// </summary>
    private readonly Dictionary<string, int> _notExistParameterId = [];

    /// <summary>
    ///     存在していないパラメータの値のリスト
    /// </summary>
    private readonly Dictionary<int, float> _notExistParameterValues = [];

    /// <summary>
    ///     存在していないパーツIDのリスト
    /// </summary>
    private readonly Dictionary<string, int> _notExistPartId = [];

    /// <summary>
    ///     存在していないパーツの不透明度のリスト
    /// </summary>
    private readonly Dictionary<int, float> _notExistPartOpacities = [];

    /// <summary>
    ///     パラメータの最大値のリスト
    /// </summary>
    private readonly unsafe float* _parameterMaximumValues;

    /// <summary>
    ///     パラメータの最小値のリスト
    /// </summary>
    private readonly unsafe float* _parameterMinimumValues;

    /// <summary>
    ///     パラメータの値のリスト
    /// </summary>
    private readonly unsafe float* _parameterValues;

    /// <summary>
    ///     Partの子DrawableIndexの配列
    /// </summary>
    private readonly List<int>[] _partChildDrawables;

    /// <summary>
    ///     パーツの不透明度のリスト
    /// </summary>
    private readonly unsafe float* _partOpacities;

    /// <summary>
    ///     保存されたパラメータ
    /// </summary>
    private readonly List<float> _savedParameters = [];

    /// <summary>
    ///     カリング設定の配列
    /// </summary>
    private readonly List<DrawableCullingData> _userCullings = [];

    /// <summary>
    ///     Drawable スクリーン色の配列
    /// </summary>
    private readonly List<DrawableColorData> _userMultiplyColors = [];

    /// <summary>
    ///     Part スクリーン色の配列
    /// </summary>
    private readonly List<PartColorData> _userPartMultiplyColors = [];

    /// <summary>
    ///     Part 乗算色の配列
    /// </summary>
    private readonly List<PartColorData> _userPartScreenColors = [];

    /// <summary>
    ///     Drawable 乗算色の配列
    /// </summary>
    private readonly List<DrawableColorData> _userScreenColors = [];

    public readonly List<string> DrawableIds = [];

    public readonly List<string> ParameterIds = [];

    public readonly List<string> PartIds = [];

    /// <summary>
    ///     モデルのカリング設定をすべて上書きするか？
    /// </summary>
    private bool _isOverwrittenCullings;

    /// <summary>
    ///     乗算色を全て上書きするか？
    /// </summary>
    private bool _isOverwrittenModelMultiplyColors;

    /// <summary>
    ///     スクリーン色を全て上書きするか？
    /// </summary>
    private bool _isOverwrittenModelScreenColors;

    /// <summary>
    ///     モデルの不透明度
    /// </summary>
    private float _modelOpacity;

    public unsafe CubismModel(IntPtr model)
    {
        Model         = model;
        _modelOpacity = 1.0f;

        _parameterValues        = CubismCore.GetParameterValues(Model);
        _partOpacities          = CubismCore.GetPartOpacities(Model);
        _parameterMaximumValues = CubismCore.GetParameterMaximumValues(Model);
        _parameterMinimumValues = CubismCore.GetParameterMinimumValues(Model);

        {
            var parameterIds   = CubismCore.GetParameterIds(Model);
            var parameterCount = CubismCore.GetParameterCount(Model);

            for ( var i = 0; i < parameterCount; ++i )
            {
                var str = new string(parameterIds[i]);
                ParameterIds.Add(CubismFramework.CubismIdManager.GetId(str));
            }
        }

        var partCount = CubismCore.GetPartCount(Model);
        var partIds   = CubismCore.GetPartIds(Model);

        _partChildDrawables = new List<int>[partCount];
        for ( var i = 0; i < partCount; ++i )
        {
            var str = new string(partIds[i]);
            PartIds.Add(CubismFramework.CubismIdManager.GetId(str));
            _partChildDrawables[i] = [];
        }

        var drawableIds   = CubismCore.GetDrawableIds(Model);
        var drawableCount = CubismCore.GetDrawableCount(Model);

        // カリング設定
        var userCulling = new DrawableCullingData { IsOverwritten = false, IsCulling = false };

        // 乗算色
        var multiplyColor = new CubismTextureColor();

        // スクリーン色
        var screenColor = new CubismTextureColor(0, 0, 0, 1.0f);

        // Parts
        for ( var i = 0; i < partCount; ++i )
        {
            _userPartMultiplyColors.Add(new PartColorData {
                                                              IsOverwritten = false, Color = multiplyColor // 乗算色
                                                          });

            _userPartScreenColors.Add(new PartColorData {
                                                            IsOverwritten = false, Color = screenColor // スクリーン色
                                                        });
        }

        // Drawables
        for ( var i = 0; i < drawableCount; ++i )
        {
            var str = new string(drawableIds[i]);
            DrawableIds.Add(CubismFramework.CubismIdManager.GetId(str));
            _userMultiplyColors.Add(new DrawableColorData {
                                                              IsOverwritten = false, Color = multiplyColor // 乗算色
                                                          });

            _userScreenColors.Add(new DrawableColorData {
                                                            IsOverwritten = false, Color = screenColor // スクリーン色
                                                        });

            _userCullings.Add(userCulling);

            var parentIndex = CubismCore.GetDrawableParentPartIndices(Model)[i];
            if ( parentIndex >= 0 )
            {
                _partChildDrawables[parentIndex].Add(i);
            }
        }
    }

    /// <summary>
    ///     モデル
    /// </summary>
    public IntPtr Model { get; }

    public void Dispose()
    {
        CubismFramework.DeallocateAligned(Model);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    ///     partのOverwriteColor Set関数
    /// </summary>
    public void SetPartColor(int                 partIndex,  float                   r, float g, float b, float a,
                             List<PartColorData> partColors, List<DrawableColorData> drawableColors)
    {
        partColors[partIndex].Color.R = r;
        partColors[partIndex].Color.G = g;
        partColors[partIndex].Color.B = b;
        partColors[partIndex].Color.A = a;

        if ( partColors[partIndex].IsOverwritten )
        {
            for ( var i = 0; i < _partChildDrawables[partIndex].Count; i++ )
            {
                var drawableIndex = _partChildDrawables[partIndex][i];
                drawableColors[drawableIndex].Color.R = r;
                drawableColors[drawableIndex].Color.G = g;
                drawableColors[drawableIndex].Color.B = b;
                drawableColors[drawableIndex].Color.A = a;
            }
        }
    }

    /// <summary>
    ///     partのOverwriteFlag Set関数
    /// </summary>
    public void SetOverwriteColorForPartColors(int                 partIndex,  bool                    value,
                                               List<PartColorData> partColors, List<DrawableColorData> drawableColors)
    {
        partColors[partIndex].IsOverwritten = value;

        for ( var i = 0; i < _partChildDrawables[partIndex].Count; i++ )
        {
            var drawableIndex = _partChildDrawables[partIndex][i];
            drawableColors[drawableIndex].IsOverwritten = value;
            if ( value )
            {
                drawableColors[drawableIndex].Color.R = partColors[partIndex].Color.R;
                drawableColors[drawableIndex].Color.G = partColors[partIndex].Color.G;
                drawableColors[drawableIndex].Color.B = partColors[partIndex].Color.B;
                drawableColors[drawableIndex].Color.A = partColors[partIndex].Color.A;
            }
        }
    }

    /// <summary>
    ///     モデルのパラメータを更新する。
    /// </summary>
    public void Update()
    {
        // Update model.
        CubismCore.UpdateModel(Model);
        // Reset dynamic drawable flags.
        CubismCore.ResetDrawableDynamicFlags(Model);
    }

    /// <summary>
    ///     Pixel単位でキャンバスの幅の取得
    /// </summary>
    /// <returns>キャンバスの幅(pixel)</returns>
    public float GetCanvasWidthPixel()
    {
        if ( Model == IntPtr.Zero )
        {
            return 0.0f;
        }

        CubismCore.ReadCanvasInfo(Model, out var tmpSizeInPixels, out _, out _);

        return tmpSizeInPixels.X;
    }

    /// <summary>
    ///     Pixel単位でキャンバスの高さの取得
    /// </summary>
    /// <returns>キャンバスの高さ(pixel)</returns>
    public float GetCanvasHeightPixel()
    {
        if ( new IntPtr(Model) == IntPtr.Zero )
        {
            return 0.0f;
        }

        CubismCore.ReadCanvasInfo(Model, out var tmpSizeInPixels, out _, out _);

        return tmpSizeInPixels.Y;
    }

    /// <summary>
    ///     PixelsPerUnitを取得する。
    /// </summary>
    /// <returns>PixelsPerUnit</returns>
    public float GetPixelsPerUnit()
    {
        if ( new IntPtr(Model) == IntPtr.Zero )
        {
            return 0.0f;
        }

        CubismCore.ReadCanvasInfo(Model, out _, out _, out var tmpPixelsPerUnit);

        return tmpPixelsPerUnit;
    }

    /// <summary>
    ///     Unit単位でキャンバスの幅の取得
    /// </summary>
    /// <returns>キャンバスの幅(Unit)</returns>
    public float GetCanvasWidth()
    {
        CubismCore.ReadCanvasInfo(Model, out var tmpSizeInPixels, out _, out var tmpPixelsPerUnit);

        return tmpSizeInPixels.X / tmpPixelsPerUnit;
    }

    /// <summary>
    ///     Unit単位でキャンバスの高さの取得
    /// </summary>
    /// <returns>キャンバスの高さ(Unit)</returns>
    public float GetCanvasHeight()
    {
        CubismCore.ReadCanvasInfo(Model, out var tmpSizeInPixels, out _, out var tmpPixelsPerUnit);

        return tmpSizeInPixels.Y / tmpPixelsPerUnit;
    }

    /// <summary>
    ///     パーツのインデックスを取得する。
    /// </summary>
    /// <param name="partId">パーツのID</param>
    /// <returns>パーツのインデックス</returns>
    public int GetPartIndex(string partId)
    {
        var partIndex = PartIds.IndexOf(partId);
        if ( partIndex != -1 )
        {
            return partIndex;
        }

        var partCount = CubismCore.GetPartCount(Model);

        // モデルに存在していない場合、非存在パーツIDリスト内にあるかを検索し、そのインデックスを返す
        if ( _notExistPartId.TryGetValue(partId, out var item) )
        {
            return item;
        }

        // 非存在パーツIDリストにない場合、新しく要素を追加する
        partIndex = partCount + _notExistPartId.Count;

        _notExistPartId.TryAdd(partId, partIndex);
        _notExistPartOpacities.Add(partIndex, 0);

        return partIndex;
    }

    /// <summary>
    ///     パーツのIDを取得する。
    /// </summary>
    /// <param name="partIndex">パーツのIndex</param>
    /// <returns>パーツのID</returns>
    public string GetPartId(int partIndex)
    {
        if ( 0 <= partIndex && partIndex < PartIds.Count )
        {
            throw new IndexOutOfRangeException("Out of PartIds size");
        }

        return PartIds[partIndex];
    }

    /// <summary>
    ///     パーツの個数を取得する。
    /// </summary>
    /// <returns>パーツの個数</returns>
    public int GetPartCount() { return CubismCore.GetPartCount(Model); }

    /// <summary>
    ///     パーツの不透明度を設定する。
    /// </summary>
    /// <param name="partId">パーツのID</param>
    /// <param name="opacity">不透明度</param>
    public void SetPartOpacity(string partId, float opacity)
    {
        // 高速化のためにPartIndexを取得できる機構になっているが、外部からの設定の時は呼び出し頻度が低いため不要
        var index = GetPartIndex(partId);

        if ( index < 0 )
        {
            return; // パーツが無いのでスキップ
        }

        SetPartOpacity(index, opacity);
    }

    /// <summary>
    ///     パーツの不透明度を設定する。
    /// </summary>
    /// <param name="partIndex">パーツのインデックス</param>
    /// <param name="opacity">パーツの不透明度</param>
    public unsafe void SetPartOpacity(int partIndex, float opacity)
    {
        if ( _notExistPartOpacities.ContainsKey(partIndex) )
        {
            _notExistPartOpacities[partIndex] = opacity;

            return;
        }

        //インデックスの範囲内検知
        if ( 0 > partIndex || partIndex >= GetPartCount() )
        {
            throw new ArgumentException("partIndex out of range");
        }

        _partOpacities[partIndex] = opacity;
    }

    /// <summary>
    ///     パーツの不透明度を取得する。
    /// </summary>
    /// <param name="partId">パーツのID</param>
    /// <returns>パーツの不透明度</returns>
    public float GetPartOpacity(string partId)
    {
        // 高速化のためにPartIndexを取得できる機構になっているが、外部からの設定の時は呼び出し頻度が低いため不要
        var index = GetPartIndex(partId);

        if ( index < 0 )
        {
            return 0; //パーツが無いのでスキップ
        }

        return GetPartOpacity(index);
    }

    /// <summary>
    ///     パーツの不透明度を取得する。
    /// </summary>
    /// <param name="partIndex">パーツのインデックス</param>
    /// <returns>パーツの不透明度</returns>
    public unsafe float GetPartOpacity(int partIndex)
    {
        if ( _notExistPartOpacities.TryGetValue(partIndex, out var value) )
        {
            // モデルに存在しないパーツIDの場合、非存在パーツリストから不透明度を返す
            return value;
        }

        //インデックスの範囲内検知
        if ( 0 > partIndex || partIndex >= GetPartCount() )
        {
            throw new ArgumentException("partIndex out of range");
        }

        return _partOpacities[partIndex];
    }

    /// <summary>
    ///     パラメータのインデックスを取得する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <returns>パラメータのインデックス</returns>
    public int GetParameterIndex(string parameterId)
    {
        var parameterIndex = ParameterIds.IndexOf(parameterId);
        if ( parameterIndex != -1 )
        {
            return parameterIndex;
        }

        // モデルに存在していない場合、非存在パラメータIDリスト内を検索し、そのインデックスを返す
        if ( _notExistParameterId.TryGetValue(parameterId, out var data) )
        {
            return data;
        }

        // 非存在パラメータIDリストにない場合、新しく要素を追加する
        parameterIndex = CubismCore.GetParameterCount(Model) + _notExistParameterId.Count;

        _notExistParameterId.TryAdd(parameterId, parameterIndex);
        _notExistParameterValues.Add(parameterIndex, 0);

        return parameterIndex;
    }

    /// <summary>
    ///     パラメータの個数を取得する。
    /// </summary>
    /// <returns>パラメータの個数</returns>
    public int GetParameterCount() { return CubismCore.GetParameterCount(Model); }

    /// <summary>
    ///     パラメータの種類を取得する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <returns>
    ///     csmParameterType_Normal -> 通常のパラメータ
    ///     csmParameterType_BlendShape -> ブレンドシェイプパラメータ
    /// </returns>
    public unsafe int GetParameterType(int parameterIndex) { return CubismCore.GetParameterTypes(Model)[parameterIndex]; }

    /// <summary>
    ///     パラメータの最大値を取得する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <returns>パラメータの最大値</returns>
    public unsafe float GetParameterMaximumValue(int parameterIndex) { return CubismCore.GetParameterMaximumValues(Model)[parameterIndex]; }

    /// <summary>
    ///     パラメータの最小値を取得する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <returns>パラメータの最小値</returns>
    public unsafe float GetParameterMinimumValue(int parameterIndex) { return CubismCore.GetParameterMinimumValues(Model)[parameterIndex]; }

    /// <summary>
    ///     パラメータのデフォルト値を取得する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <returns> パラメータのデフォルト値</returns>
    public unsafe float GetParameterDefaultValue(int parameterIndex) { return CubismCore.GetParameterDefaultValues(Model)[parameterIndex]; }

    /// <summary>
    ///     パラメータの値を取得する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <returns>パラメータの値</returns>
    public float GetParameterValue(string parameterId)
    {
        // 高速化のためにParameterIndexを取得できる機構になっているが、外部からの設定の時は呼び出し頻度が低いため不要
        var parameterIndex = GetParameterIndex(parameterId);

        return GetParameterValue(parameterIndex);
    }

    /// <summary>
    ///     パラメータの値を取得する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <returns>パラメータの値</returns>
    public unsafe float GetParameterValue(int parameterIndex)
    {
        if ( _notExistParameterValues.TryGetValue(parameterIndex, out var item) )
        {
            return item;
        }

        //インデックスの範囲内検知
        if ( 0 > parameterIndex || parameterIndex >= GetParameterCount() )
        {
            throw new ArgumentException("parameterIndex out of range");
        }

        return _parameterValues[parameterIndex];
    }

    /// <summary>
    ///     パラメータの値を設定する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <param name="value">パラメータの値</param>
    /// <param name="weight">重み</param>
    public void SetParameterValue(string parameterId, float value, float weight = 1.0f)
    {
        var index = GetParameterIndex(parameterId);
        SetParameterValue(index, value, weight);
    }

    /// <summary>
    ///     パラメータの値を設定する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <param name="value">パラメータの値</param>
    /// <param name="weight">重み</param>
    public unsafe void SetParameterValue(int parameterIndex, float value, float weight = 1.0f)
    {
        if ( _notExistParameterValues.TryGetValue(parameterIndex, out var value1) )
        {
            _notExistParameterValues[parameterIndex] = weight == 1
                                                           ? value
                                                           : value1 * (1 - weight) + value * weight;

            return;
        }

        //インデックスの範囲内検知
        if ( 0 > parameterIndex || parameterIndex >= GetParameterCount() )
        {
            throw new ArgumentException("parameterIndex out of range");
        }

        if ( CubismCore.GetParameterMaximumValues(Model)[parameterIndex] < value )
        {
            value = CubismCore.GetParameterMaximumValues(Model)[parameterIndex];
        }

        if ( CubismCore.GetParameterMinimumValues(Model)[parameterIndex] > value )
        {
            value = CubismCore.GetParameterMinimumValues(Model)[parameterIndex];
        }

        _parameterValues[parameterIndex] = weight == 1
                                               ? value
                                               : _parameterValues[parameterIndex] = _parameterValues[parameterIndex] * (1 - weight) + value * weight;
    }

    /// <summary>
    ///     パラメータの値を加算する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <param name="value">加算する値</param>
    /// <param name="weight">重み</param>
    public void AddParameterValue(string parameterId, float value, float weight = 1.0f)
    {
        var index = GetParameterIndex(parameterId);
        AddParameterValue(index, value, weight);
    }

    /// <summary>
    ///     パラメータの値を加算する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <param name="value">加算する値</param>
    /// <param name="weight">重み</param>
    public void AddParameterValue(int parameterIndex, float value, float weight = 1.0f)
    {
        if ( parameterIndex == -1 )
        {
            return;
        }

        SetParameterValue(parameterIndex, GetParameterValue(parameterIndex) + value * weight);
    }

    /// <summary>
    ///     パラメータの値を乗算する。
    /// </summary>
    /// <param name="parameterId">パラメータID</param>
    /// <param name="value">乗算する値</param>
    /// <param name="weight">重み</param>
    public void MultiplyParameterValue(string parameterId, float value, float weight = 1.0f)
    {
        var index = GetParameterIndex(parameterId);
        MultiplyParameterValue(index, value, weight);
    }

    /// <summary>
    ///     パラメータの値を乗算する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのインデックス</param>
    /// <param name="value">乗算する値</param>
    /// <param name="weight">重み</param>
    public void MultiplyParameterValue(int parameterIndex, float value, float weight = 1.0f)
    {
        if ( parameterIndex == -1 )
        {
            return;
        }

        SetParameterValue(parameterIndex, GetParameterValue(parameterIndex) * (1.0f + (value - 1.0f) * weight));
    }

    /// <summary>
    ///     Drawableのインデックスを取得する。
    /// </summary>
    /// <param name="drawableId">DrawableのID</param>
    /// <returns>Drawableのインデックス</returns>
    public int GetDrawableIndex(string drawableId) { return DrawableIds.IndexOf(drawableId); }

    /// <summary>
    ///     Drawableの個数を取得する。
    /// </summary>
    /// <returns>Drawableの個数</returns>
    public int GetDrawableCount() { return CubismCore.GetDrawableCount(Model); }

    /// <summary>
    ///     DrawableのIDを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>DrawableのID</returns>
    public string GetDrawableId(int drawableIndex)
    {
        if ( 0 <= drawableIndex && drawableIndex < DrawableIds.Count )
        {
            throw new IndexOutOfRangeException("Out of DrawableIds size");
        }

        return DrawableIds[drawableIndex];
    }

    /// <summary>
    ///     Drawableの描画順リストを取得する。
    /// </summary>
    /// <returns>Drawableの描画順リスト</returns>
    public unsafe int* GetDrawableRenderOrders() { return CubismCore.GetDrawableRenderOrders(Model); }

    /// <summary>
    ///     Drawableのテクスチャインデックスリストの取得
    ///     関数名が誤っていたため、代替となる getDrawableTextureIndex を追加し、この関数は非推奨となりました。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableのテクスチャインデックスリスト</returns>
    public int GetDrawableTextureIndices(int drawableIndex) { return GetDrawableTextureIndex(drawableIndex); }

    /// <summary>
    ///     Drawableのテクスチャインデックスを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableのテクスチャインデックス</returns>
    public unsafe int GetDrawableTextureIndex(int drawableIndex)
    {
        var textureIndices = CubismCore.GetDrawableTextureIndices(Model);

        return textureIndices[drawableIndex];
    }

    /// <summary>
    ///     Drawableの頂点インデックスの個数を取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの頂点インデックスの個数</returns>
    public unsafe int GetDrawableVertexIndexCount(int drawableIndex) { return CubismCore.GetDrawableIndexCounts(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableの頂点の個数を取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの頂点の個数</returns>
    public unsafe int GetDrawableVertexCount(int drawableIndex) { return CubismCore.GetDrawableVertexCounts(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableの頂点リストを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの頂点リスト</returns>
    public unsafe float* GetDrawableVertices(int drawableIndex) { return (float*)GetDrawableVertexPositions(drawableIndex); }

    /// <summary>
    ///     Drawableの頂点インデックスリストを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの頂点インデックスリスト</returns>
    public unsafe ushort* GetDrawableVertexIndices(int drawableIndex) { return CubismCore.GetDrawableIndices(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableの頂点リストを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの頂点リスト</returns>
    public unsafe Vector2* GetDrawableVertexPositions(int drawableIndex) { return CubismCore.GetDrawableVertexPositions(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableの頂点のUVリストを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの頂点のUVリスト</returns>
    public unsafe Vector2* GetDrawableVertexUvs(int drawableIndex) { return CubismCore.GetDrawableVertexUvs(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableの不透明度を取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの不透明度</returns>
    public unsafe float GetDrawableOpacity(int drawableIndex) { return CubismCore.GetDrawableOpacities(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableの乗算色を取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableの乗算色</returns>
    public unsafe Vector4 GetDrawableMultiplyColor(int drawableIndex) { return CubismCore.GetDrawableMultiplyColors(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableのスクリーン色を取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableのスクリーン色</returns>
    public unsafe Vector4 GetDrawableScreenColor(int drawableIndex) { return CubismCore.GetDrawableScreenColors(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableの親パーツのインデックスを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>drawableの親パーツのインデックス</returns>
    public unsafe int GetDrawableParentPartIndex(int drawableIndex) { return CubismCore.GetDrawableParentPartIndices(Model)[drawableIndex]; }

    /// <summary>
    ///     Drawableのブレンドモードを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableのブレンドモード</returns>
    public unsafe CubismBlendMode GetDrawableBlendMode(int drawableIndex)
    {
        var constantFlags = CubismCore.GetDrawableConstantFlags(Model)[drawableIndex];

        return IsBitSet(constantFlags, CsmEnum.CsmBlendAdditive)
                   ? CubismBlendMode.Additive
                   : IsBitSet(constantFlags, CsmEnum.CsmBlendMultiplicative)
                       ? CubismBlendMode.Multiplicative
                       : CubismBlendMode.Normal;
    }

    /// <summary>
    ///     Drawableのマスク使用時の反転設定を取得する。
    ///     マスクを使用しない場合は無視される
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableのマスクの反転設定</returns>
    public unsafe bool GetDrawableInvertedMask(int drawableIndex)
    {
        var constantFlags = CubismCore.GetDrawableConstantFlags(Model)[drawableIndex];

        return IsBitSet(constantFlags, CsmEnum.CsmIsInvertedMask);
    }

    /// <summary>
    ///     Drawableの表示情報を取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>
    ///     true    Drawableが表示
    ///     false   Drawableが非表示
    /// </returns>
    public unsafe bool GetDrawableDynamicFlagIsVisible(int drawableIndex)
    {
        var dynamicFlags = CubismCore.GetDrawableDynamicFlags(Model)[drawableIndex];

        return IsBitSet(dynamicFlags, CsmEnum.CsmIsVisible);
    }

    /// <summary>
    ///     直近のCubismModel::Update関数でDrawableの表示状態が変化したかを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>
    ///     true    Drawableの表示状態が直近のCubismModel::Update関数で変化した
    ///     false   Drawableの表示状態が直近のCubismModel::Update関数で変化していない
    /// </returns>
    public unsafe bool GetDrawableDynamicFlagVisibilityDidChange(int drawableIndex)
    {
        var dynamicFlags = CubismCore.GetDrawableDynamicFlags(Model)[drawableIndex];

        return IsBitSet(dynamicFlags, CsmEnum.CsmVisibilityDidChange);
    }

    /// <summary>
    ///     直近のCubismModel::Update関数でDrawableの不透明度が変化したかを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>
    ///     true    Drawableの不透明度が直近のCubismModel::Update関数で変化した
    ///     false   Drawableの不透明度が直近のCubismModel::Update関数で変化していない
    /// </returns>
    public unsafe bool GetDrawableDynamicFlagOpacityDidChange(int drawableIndex)
    {
        var dynamicFlags = CubismCore.GetDrawableDynamicFlags(Model)[drawableIndex];

        return IsBitSet(dynamicFlags, CsmEnum.CsmOpacityDidChange);
    }

    /// <summary>
    ///     直近のCubismModel::Update関数でDrawableのDrawOrderが変化したかを取得する。
    ///     DrawOrderはArtMesh上で指定する0から1000の情報
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>
    ///     true    Drawableの不透明度が直近のCubismModel::Update関数で変化した
    ///     false   Drawableの不透明度が直近のCubismModel::Update関数で変化していない
    /// </returns>
    public unsafe bool GetDrawableDynamicFlagDrawOrderDidChange(int drawableIndex)
    {
        var dynamicFlags = CubismCore.GetDrawableDynamicFlags(Model)[drawableIndex];

        return IsBitSet(dynamicFlags, CsmEnum.CsmDrawOrderDidChange);
    }

    /// <summary>
    ///     直近のCubismModel::Update関数でDrawableの描画の順序が変化したかを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>
    ///     true    Drawableの描画の順序が直近のCubismModel::Update関数で変化した
    ///     false   Drawableの描画の順序が直近のCubismModel::Update関数で変化していない
    /// </returns>
    public unsafe bool GetDrawableDynamicFlagRenderOrderDidChange(int drawableIndex)
    {
        var dynamicFlags = CubismCore.GetDrawableDynamicFlags(Model)[drawableIndex];

        return IsBitSet(dynamicFlags, CsmEnum.CsmRenderOrderDidChange);
    }

    /// <summary>
    ///     直近のCubismModel::Update関数でDrawableの頂点情報が変化したかを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>
    ///     true    Drawableの頂点情報が直近のCubismModel::Update関数で変化した
    ///     false   Drawableの頂点情報が直近のCubismModel::Update関数で変化していない
    /// </returns>
    public unsafe bool GetDrawableDynamicFlagVertexPositionsDidChange(int drawableIndex)
    {
        var dynamicFlags = CubismCore.GetDrawableDynamicFlags(Model)[drawableIndex];

        return IsBitSet(dynamicFlags, CsmEnum.CsmVertexPositionsDidChange);
    }

    /// <summary>
    ///     直近のCubismModel::Update関数でDrawableの乗算色・スクリーン色が変化したかを取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>
    ///     true    Drawableの乗算色・スクリーン色が直近のCubismModel::Update関数で変化した
    ///     false   Drawableの乗算色・スクリーン色が直近のCubismModel::Update関数で変化していない
    /// </returns>
    public unsafe bool GetDrawableDynamicFlagBlendColorDidChange(int drawableIndex)
    {
        var dynamicFlags = CubismCore.GetDrawableDynamicFlags(Model)[drawableIndex];

        return IsBitSet(dynamicFlags, CsmEnum.CsmBlendColorDidChange);
    }

    /// <summary>
    ///     Drawableのクリッピングマスクリストを取得する。
    /// </summary>
    /// <returns>Drawableのクリッピングマスクリスト</returns>
    public unsafe int** GetDrawableMasks() { return CubismCore.GetDrawableMasks(Model); }

    /// <summary>
    ///     Drawableのクリッピングマスクの個数リストを取得する。
    /// </summary>
    /// <returns>Drawableのクリッピングマスクの個数リスト</returns>
    public unsafe int* GetDrawableMaskCounts() { return CubismCore.GetDrawableMaskCounts(Model); }

    /// <summary>
    ///     クリッピングマスクを使用しているかどうか？
    /// </summary>
    /// <returns>
    ///     true    クリッピングマスクを使用している
    ///     false   クリッピングマスクを使用していない
    /// </returns>
    public unsafe bool IsUsingMasking()
    {
        for ( var d = 0; d < CubismCore.GetDrawableCount(Model); ++d )
        {
            if ( CubismCore.GetDrawableMaskCounts(Model)[d] <= 0 )
            {
                continue;
            }

            return true;
        }

        return false;
    }

    /// <summary>
    ///     保存されたパラメータを読み込む
    /// </summary>
    public unsafe void LoadParameters()
    {
        var parameterCount      = CubismCore.GetParameterCount(Model);
        var savedParameterCount = _savedParameters.Count;

        if ( parameterCount > savedParameterCount )
        {
            parameterCount = savedParameterCount;
        }

        for ( var i = 0; i < parameterCount; ++i )
        {
            _parameterValues[i] = _savedParameters[i];
        }
    }

    /// <summary>
    ///     パラメータを保存する。
    /// </summary>
    public unsafe void SaveParameters()
    {
        var parameterCount      = CubismCore.GetParameterCount(Model);
        var savedParameterCount = _savedParameters.Count;

        if ( savedParameterCount != parameterCount )
        {
            _savedParameters.Clear();
            for ( var i = 0; i < parameterCount; ++i )
            {
                _savedParameters.Add(_parameterValues[i]);
            }
        }
        else
        {
            for ( var i = 0; i < parameterCount; ++i )
            {
                _savedParameters[i] = _parameterValues[i];
            }
        }
    }

    /// <summary>
    ///     drawableの乗算色を取得する
    /// </summary>
    public CubismTextureColor GetMultiplyColor(int drawableIndex)
    {
        if ( GetOverwriteFlagForModelMultiplyColors() ||
             GetOverwriteFlagForDrawableMultiplyColors(drawableIndex) )
        {
            return _userMultiplyColors[drawableIndex].Color;
        }

        var color = GetDrawableMultiplyColor(drawableIndex);

        return new CubismTextureColor(color.X, color.Y, color.Z, color.W);
    }

    /// <summary>
    ///     drawableのスクリーン色を取得する
    /// </summary>
    public CubismTextureColor GetScreenColor(int drawableIndex)
    {
        if ( GetOverwriteFlagForModelScreenColors() ||
             GetOverwriteFlagForDrawableScreenColors(drawableIndex) )
        {
            return _userScreenColors[drawableIndex].Color;
        }

        var color = GetDrawableScreenColor(drawableIndex);

        return new CubismTextureColor(color.X, color.Y, color.Z, color.W);
    }

    /// <summary>
    ///     drawableの乗算色を設定する
    /// </summary>
    public void SetMultiplyColor(int drawableIndex, CubismTextureColor color) { SetMultiplyColor(drawableIndex, color.R, color.G, color.B, color.A); }

    /// <summary>
    ///     drawableの乗算色を設定する
    /// </summary>
    public void SetMultiplyColor(int drawableIndex, float r, float g, float b, float a = 1.0f)
    {
        _userMultiplyColors[drawableIndex].Color.R = r;
        _userMultiplyColors[drawableIndex].Color.G = g;
        _userMultiplyColors[drawableIndex].Color.B = b;
        _userMultiplyColors[drawableIndex].Color.A = a;
    }

    /// <summary>
    ///     drawableのスクリーン色を設定する
    /// </summary>
    /// <param name="drawableIndex"></param>
    /// <param name="color"></param>
    public void SetScreenColor(int drawableIndex, CubismTextureColor color) { SetScreenColor(drawableIndex, color.R, color.G, color.B, color.A); }

    /// <summary>
    ///     drawableのスクリーン色を設定する
    /// </summary>
    public void SetScreenColor(int drawableIndex, float r, float g, float b, float a = 1.0f)
    {
        _userScreenColors[drawableIndex].Color.R = r;
        _userScreenColors[drawableIndex].Color.G = g;
        _userScreenColors[drawableIndex].Color.B = b;
        _userScreenColors[drawableIndex].Color.A = a;
    }

    /// <summary>
    ///     partの乗算色を取得する
    /// </summary>
    public CubismTextureColor GetPartMultiplyColor(int partIndex) { return _userPartMultiplyColors[partIndex].Color; }

    /// <summary>
    ///     partの乗算色を取得する
    /// </summary>
    public CubismTextureColor GetPartScreenColor(int partIndex) { return _userPartScreenColors[partIndex].Color; }

    /// <summary>
    ///     partのスクリーン色を設定する
    /// </summary>
    public void SetPartMultiplyColor(int partIndex, CubismTextureColor color) { SetPartMultiplyColor(partIndex, color.R, color.G, color.B, color.A); }

    /// <summary>
    ///     partの乗算色を設定する
    /// </summary>
    public void SetPartMultiplyColor(int partIndex, float r, float g, float b, float a = 1.0f) { SetPartColor(partIndex, r, g, b, a, _userPartMultiplyColors, _userMultiplyColors); }

    /// <summary>
    ///     partのスクリーン色を設定する
    /// </summary>
    public void SetPartScreenColor(int partIndex, CubismTextureColor color) { SetPartScreenColor(partIndex, color.R, color.G, color.B, color.A); }

    /// <summary>
    ///     partのスクリーン色を設定する
    /// </summary>
    /// <param name="a"></param>
    public void SetPartScreenColor(int partIndex, float r, float g, float b, float a = 1.0f) { SetPartColor(partIndex, r, g, b, a, _userPartScreenColors, _userScreenColors); }

    /// <summary>
    ///     SDKからモデル全体の乗算色を上書きするか。
    /// </summary>
    /// <returns>
    ///     true    ->  SDK上の色情報を使用
    ///     false   ->  モデルの色情報を使用
    /// </returns>
    public bool GetOverwriteFlagForModelMultiplyColors() { return _isOverwrittenModelMultiplyColors; }

    /// <summary>
    ///     SDKからモデル全体のスクリーン色を上書きするか。
    /// </summary>
    /// <returns>
    ///     true    ->  SDK上の色情報を使用
    ///     false   ->  モデルの色情報を使用
    /// </returns>
    public bool GetOverwriteFlagForModelScreenColors() { return _isOverwrittenModelScreenColors; }

    /// <summary>
    ///     SDKからモデル全体の乗算色を上書きするかをセットする
    ///     SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse
    /// </summary>
    public void SetOverwriteFlagForModelMultiplyColors(bool value) { _isOverwrittenModelMultiplyColors = value; }

    /// <summary>
    ///     SDKからモデル全体のスクリーン色を上書きするかをセットする
    ///     SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse
    /// </summary>
    public void SetOverwriteFlagForModelScreenColors(bool value) { _isOverwrittenModelScreenColors = value; }

    /// <summary>
    ///     SDKからdrawableの乗算色を上書きするか。
    /// </summary>
    /// <returns>
    ///     true    ->  SDK上の色情報を使用
    ///     false   ->  モデルの色情報を使用
    /// </returns>
    public bool GetOverwriteFlagForDrawableMultiplyColors(int drawableIndex) { return _userMultiplyColors[drawableIndex].IsOverwritten; }

    /// <summary>
    ///     SDKからdrawableのスクリーン色を上書きするか。
    /// </summary>
    /// <returns>
    ///     true    ->  SDK上の色情報を使用
    ///     false   ->  モデルの色情報を使用
    /// </returns>
    public bool GetOverwriteFlagForDrawableScreenColors(int drawableIndex) { return _userScreenColors[drawableIndex].IsOverwritten; }

    /// <summary>
    ///     SDKからdrawableの乗算色を上書きするかをセットする
    ///     SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse
    /// </summary>
    public void SetOverwriteFlagForDrawableMultiplyColors(int drawableIndex, bool value) { _userMultiplyColors[drawableIndex].IsOverwritten = value; }

    /// <summary>
    ///     SDKからdrawableのスクリーン色を上書きするかをセットする
    ///     SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse
    /// </summary>
    public void SetOverwriteFlagForDrawableScreenColors(int drawableIndex, bool value) { _userScreenColors[drawableIndex].IsOverwritten = value; }

    /// <summary>
    ///     SDKからpartの乗算色を上書きするか。
    /// </summary>
    /// <returns>
    ///     true    ->  SDK上の色情報を使用
    ///     false   ->  モデルの色情報を使用
    /// </returns>
    public bool GetOverwriteColorForPartMultiplyColors(int partIndex) { return _userPartMultiplyColors[partIndex].IsOverwritten; }

    /// <summary>
    ///     SDKからpartのスクリーン色を上書きするかをセットする
    ///     SDK上の色情報を使うならtrue、モデルの色情報を使うならfalse
    /// </summary>
    public bool GetOverwriteColorForPartScreenColors(int partIndex) { return _userPartScreenColors[partIndex].IsOverwritten; }

    /// <summary>
    ///     Drawableのカリング情報を取得する。
    /// </summary>
    /// <param name="drawableIndex">Drawableのインデックス</param>
    /// <returns>Drawableのカリング情報</returns>
    public unsafe bool GetDrawableCulling(int drawableIndex)
    {
        if ( GetOverwriteFlagForModelCullings() || GetOverwriteFlagForDrawableCullings(drawableIndex) )
        {
            return _userCullings[drawableIndex].IsCulling;
        }

        var constantFlags = CubismCore.GetDrawableConstantFlags(Model);

        return !IsBitSet(constantFlags[drawableIndex], CsmEnum.CsmIsDoubleSided);
    }

    /// <summary>
    ///     Drawableのカリング情報を設定する
    /// </summary>
    public void SetDrawableCulling(int drawableIndex, bool isCulling) { _userCullings[drawableIndex].IsCulling = isCulling; }

    /// <summary>
    ///     SDKからモデル全体のカリング設定を上書きするか。
    /// </summary>
    /// <returns>
    ///     true    ->  SDK上のカリング設定を使用
    ///     false   ->  モデルのカリング設定を使用
    /// </returns>
    public bool GetOverwriteFlagForModelCullings() { return _isOverwrittenCullings; }

    /// <summary>
    ///     SDKからモデル全体のカリング設定を上書きするかをセットする
    ///     SDK上のカリング設定を使うならtrue、モデルのカリング設定を使うならfalse
    /// </summary>
    public void SetOverwriteFlagForModelCullings(bool value) { _isOverwrittenCullings = value; }

    /// <summary>
    ///     SDKからdrawableのカリング設定を上書きするか。
    /// </summary>
    /// <returns>
    ///     true    ->  SDK上のカリング設定を使用
    ///     false   ->  モデルのカリング設定を使用
    /// </returns>
    public bool GetOverwriteFlagForDrawableCullings(int drawableIndex) { return _userCullings[drawableIndex].IsOverwritten; }

    /// <summary>
    ///     SDKからdrawableのカリング設定を上書きするかをセットする
    ///     SDK上のカリング設定を使うならtrue、モデルのカリング設定を使うならfalse
    /// </summary>
    public void SetOverwriteFlagForDrawableCullings(int drawableIndex, bool value) { _userCullings[drawableIndex].IsOverwritten = value; }

    /// <summary>
    ///     モデルの不透明度を取得する
    /// </summary>
    /// <returns>不透明度の値</returns>
    public float GetModelOpacity() { return _modelOpacity; }

    /// <summary>
    ///     モデルの不透明度を設定する
    /// </summary>
    /// <param name="value">不透明度の値</param>
    public void SetModelOpacity(float value) { _modelOpacity = value; }

    private static bool IsBitSet(byte data, byte mask) { return (data & mask) == mask; }

    public void SetOverwriteColorForPartMultiplyColors(int partIndex, bool value)
    {
        _userPartMultiplyColors[partIndex].IsOverwritten = value;
        SetOverwriteColorForPartColors(partIndex, value, _userPartMultiplyColors, _userMultiplyColors);
    }

    public void SetOverwriteColorForPartScreenColors(int partIndex, bool value)
    {
        _userPartScreenColors[partIndex].IsOverwritten = value;
        SetOverwriteColorForPartColors(partIndex, value, _userPartScreenColors, _userScreenColors);
    }

    /// <summary>
    ///     パラメータのIDを取得する。
    /// </summary>
    /// <param name="parameterIndex">パラメータのIndex</param>
    /// <returns>パラメータのID</returns>
    public string GetParameterId(int parameterIndex)
    {
        if ( 0 <= parameterIndex && parameterIndex < ParameterIds.Count )
        {
            throw new IndexOutOfRangeException("Out of ParameterIds size");
        }

        return ParameterIds[parameterIndex];
    }
}

namespace PersonaEngine.Lib.Live2D.Framework.Math;

/// <summary>
///     顔の向きの制御機能を提供するクラス。
/// </summary>
public class CubismTargetPoint
{
    public const int FrameRate = 30;

    public const float Epsilon = 0.01f;

    /// <summary>
    ///     顔の向きのX目標値(この値に近づいていく)
    /// </summary>
    private float _faceTargetX;

    /// <summary>
    ///     顔の向きのY目標値(この値に近づいていく)
    /// </summary>
    private float _faceTargetY;

    /// <summary>
    ///     顔の向きの変化速度X
    /// </summary>
    private float _faceVX;

    /// <summary>
    ///     顔の向きの変化速度Y
    /// </summary>
    private float _faceVY;

    /// <summary>
    ///     最後の実行時間[秒]
    /// </summary>
    private float _lastTimeSeconds;

    /// <summary>
    ///     デルタ時間の積算値[秒]
    /// </summary>
    private float _userTimeSeconds;

    /// <summary>
    ///     顔の向きX(-1.0 - 1.0)
    /// </summary>
    public float FaceX { get; private set; }

    /// <summary>
    ///     顔の向きY(-1.0 - 1.0)
    /// </summary>
    public float FaceY { get; private set; }

    /// <summary>
    ///     更新処理を行う。
    /// </summary>
    /// <param name="deltaTimeSeconds">デルタ時間[秒]</param>
    public void Update(float deltaTimeSeconds)
    {
        // デルタ時間を加算する
        _userTimeSeconds += deltaTimeSeconds;

        // 首を中央から左右に振るときの平均的な早さは  秒程度。加速・減速を考慮して、その2倍を最高速度とする
        // 顔のふり具合を、中央(0.0)から、左右は(+-1.0)とする
        var FaceParamMaxV = 40.0f / 10.0f;                    // 7.5秒間に40分移動（5.3/sc)
        var MaxV          = FaceParamMaxV * 1.0f / FrameRate; // 1frameあたりに変化できる速度の上限

        if ( _lastTimeSeconds == 0.0f )
        {
            _lastTimeSeconds = _userTimeSeconds;

            return;
        }

        var deltaTimeWeight = (_userTimeSeconds - _lastTimeSeconds) * FrameRate;
        _lastTimeSeconds = _userTimeSeconds;

        // 最高速度になるまでの時間を
        var TimeToMaxSpeed  = 0.15f;
        var FrameToMaxSpeed = TimeToMaxSpeed * FrameRate;               // sec * frame/sec
        var MaxA            = deltaTimeWeight * MaxV / FrameToMaxSpeed; // 1frameあたりの加速度

        // 目指す向きは、(dx, dy)方向のベクトルとなる
        var dx = _faceTargetX - FaceX;
        var dy = _faceTargetY - FaceY;

        if ( MathF.Abs(dx) <= Epsilon && MathF.Abs(dy) <= Epsilon )
        {
            return; // 変化なし
        }

        // 速度の最大よりも大きい場合は、速度を落とす
        var d = MathF.Sqrt(dx * dx + dy * dy);

        // 進行方向の最大速度ベクトル
        var vx = MaxV * dx / d;
        var vy = MaxV * dy / d;

        // 現在の速度から、新規速度への変化（加速度）を求める
        var ax = vx - _faceVX;
        var ay = vy - _faceVY;

        var a = MathF.Sqrt(ax * ax + ay * ay);

        // 加速のとき
        if ( a < -MaxA || a > MaxA )
        {
            ax *= MaxA / a;
            ay *= MaxA / a;
        }

        // 加速度を元の速度に足して、新速度とする
        _faceVX += ax;
        _faceVY += ay;

        // 目的の方向に近づいたとき、滑らかに減速するための処理
        // 設定された加速度で止まることのできる距離と速度の関係から
        // 現在とりうる最高速度を計算し、それ以上のときは速度を落とす
        // ※本来、人間は筋力で力（加速度）を調整できるため、より自由度が高いが、簡単な処理ですませている
        {
            // 加速度、速度、距離の関係式。
            //            2  6           2               3
            //      sqrt(a  t  + 16 a h t  - 8 a h) - a t
            // v = --------------------------------------
            //                    2
            //                 4 t  - 2
            // (t=1)
            //  時刻tは、あらかじめ加速度、速度を1/60(フレームレート、単位なし)で
            //  考えているので、t＝１として消してよい（※未検証）

            var maxV = 0.5f * (MathF.Sqrt(MaxA * MaxA + 16.0f * MaxA * d - 8.0f * MaxA * d) - MaxA);
            var curV = MathF.Sqrt(_faceVX * _faceVX + _faceVY * _faceVY);

            if ( curV > maxV )
            {
                // 現在の速度 > 最高速度のとき、最高速度まで減速
                _faceVX *= maxV / curV;
                _faceVY *= maxV / curV;
            }
        }

        FaceX += _faceVX;
        FaceY += _faceVY;
    }

    /// <summary>
    ///     顔の向きの目標値を設定する。
    /// </summary>
    /// <param name="x">X軸の顔の向きの値(-1.0 - 1.0)</param>
    /// <param name="y">Y軸の顔の向きの値(-1.0 - 1.0)</param>
    public void Set(float x, float y)
    {
        _faceTargetX = x;
        _faceTargetY = y;
    }
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     A strategy for aggregating multiple audio channels into a single channel.
/// </summary>
public interface IChannelAggregationStrategy
{
    /// <summary>
    ///     Aggregates the specified frame of audio samples.
    /// </summary>
    void Aggregate(ReadOnlyMemory<float> frame, Memory<float> destination);

    void Aggregate(ReadOnlyMemory<byte> frame, Memory<byte> destination, ushort bitsPerSample);

    void Aggregate(ReadOnlyMemory<byte> frame, Memory<float> destination, ushort bitsPerSample);

    void Aggregate(ReadOnlyMemory<float> frame, Memory<byte> destination, ushort bitsPerSample);
}

/// <summary>
///     Provides predefined strategies for aggregating float audio channels.
/// </summary>
public static class DefaultChannelAggregationStrategies
{
    /// <summary>
    ///     Gets the average aggregation strategy for float audio channels.
    /// </summary>
    public static IChannelAggregationStrategy Average { get; } = new AverageChannelAggregationStrategy();

    /// <summary>
    ///     Gets the max aggregation strategy for float audio channels.
    /// </summary>
    public static IChannelAggregationStrategy Sum { get; } = new SumChannelAggregationStrategy();

    /// <summary>
    ///     Gets the channel selection strategy for float audio channels.
    /// </summary>
    /// <param name="channelIndex">The index of the channel to use for aggregation.</param>
    /// <returns>The channel selection strategy.</returns>
    public static IChannelAggregationStrategy SelectChannel(int channelIndex) { return new SelectChannelAggregationStrategy(channelIndex); }
}

/// <summary>
///     Aggregates audio channels by computing the average of the samples.
/// </summary>
internal class AverageChannelAggregationStrategy : IChannelAggregationStrategy
{
    public void Aggregate(ReadOnlyMemory<float> frame, Memory<float> destination) { destination.Span[0] = GetAverageFloat(frame); }

    public void Aggregate(ReadOnlyMemory<byte> frame, Memory<byte> destination, ushort bitsPerSample)
    {
        var sampleValue = GetAverageFloat(frame, bitsPerSample);
        SampleSerializer.WriteSample(destination.Span, 0, sampleValue, bitsPerSample);
    }

    public void Aggregate(ReadOnlyMemory<byte> frame, Memory<float> destination, ushort bitsPerSample) { destination.Span[0] = GetAverageFloat(frame, bitsPerSample); }

    public void Aggregate(ReadOnlyMemory<float> frame, Memory<byte> destination, ushort bitsPerSample)
    {
        var sampleValue = GetAverageFloat(frame);
        SampleSerializer.WriteSample(destination.Span, 0, sampleValue, bitsPerSample);
    }

    private static float GetAverageFloat(ReadOnlyMemory<float> frame)
    {
        var span = frame.Span;
        var sum  = 0.0f;
        for ( var i = 0; i < span.Length; i++ )
        {
            sum += span[i];
        }

        return sum / span.Length;
    }

    private static float GetAverageFloat(ReadOnlyMemory<byte> frame, ushort bitsPerSample)
    {
        var index = 0;
        var sum   = 0f;
        var span  = frame.Span;
        while ( index < frame.Length )
        {
            sum += SampleSerializer.ReadSample(span, ref index, bitsPerSample);
        }

        // We multiply by 8 first to avoid integer division
        return sum / (frame.Length * 8 / bitsPerSample);
    }
}

/// <summary>
///     Aggregates float audio channels by selecting the maximum sample.
/// </summary>
internal class SumChannelAggregationStrategy : IChannelAggregationStrategy
{
    public void Aggregate(ReadOnlyMemory<float> frame, Memory<float> destination) { destination.Span[0] = GetSumFloat(frame); }

    public void Aggregate(ReadOnlyMemory<byte> frame, Memory<byte> destination, ushort bitsPerSample)
    {
        var sampleValue = GetSumFloat(frame, bitsPerSample);
        SampleSerializer.WriteSample(destination.Span, 0, sampleValue, bitsPerSample);
    }

    public void Aggregate(ReadOnlyMemory<byte> frame, Memory<float> destination, ushort bitsPerSample) { destination.Span[0] = GetSumFloat(frame, bitsPerSample); }

    public void Aggregate(ReadOnlyMemory<float> frame, Memory<byte> destination, ushort bitsPerSample)
    {
        var sampleValue = GetSumFloat(frame);
        SampleSerializer.WriteSample(destination.Span, 0, sampleValue, bitsPerSample);
    }

    private static float GetSumFloat(ReadOnlyMemory<float> frame)
    {
        var span = frame.Span;
        var sum  = 0.0f;
        for ( var i = 0; i < span.Length; i++ )
        {
            sum += span[i];
        }

        // Ensure the float value is within the range (-1, 1)
        if ( sum > 1 )
        {
            sum = 1;
        }
        else if ( sum < -1 )
        {
            sum = -1;
        }

        return sum;
    }

    private static float GetSumFloat(ReadOnlyMemory<byte> frame, ushort bitsPerSample)
    {
        var index = 0;
        var sum   = 0f;
        var span  = frame.Span;
        while ( index < frame.Length )
        {
            sum += SampleSerializer.ReadSample(span, ref index, bitsPerSample);
        }

        // Ensure the float value is within the range (-1, 1)
        if ( sum > 1 )
        {
            sum = 1;
        }
        else if ( sum < -1 )
        {
            sum = -1;
        }

        return sum;
    }
}

/// <summary>
///     Aggregates float audio channels by selecting a specific channel.
/// </summary>
/// <param name="channelIndex">The index of the channel to use for aggregation.</param>
internal class SelectChannelAggregationStrategy(int channelIndex) : IChannelAggregationStrategy
{
    public void Aggregate(ReadOnlyMemory<float> frame, Memory<float> destination) { destination.Span[0] = GetChannelFloat(frame, channelIndex); }

    public void Aggregate(ReadOnlyMemory<byte> frame, Memory<byte> destination, ushort bitsPerSample)
    {
        var sampleValue = GetChannelFloat(frame, channelIndex, bitsPerSample);
        SampleSerializer.WriteSample(destination.Span, 0, sampleValue, bitsPerSample);
    }

    public void Aggregate(ReadOnlyMemory<byte> frame, Memory<float> destination, ushort bitsPerSample) { destination.Span[0] = GetChannelFloat(frame, channelIndex, bitsPerSample); }

    public void Aggregate(ReadOnlyMemory<float> frame, Memory<byte> destination, ushort bitsPerSample)
    {
        var sampleValue = GetChannelFloat(frame, channelIndex);
        SampleSerializer.WriteSample(destination.Span, 0, sampleValue, bitsPerSample);
    }

    private static float GetChannelFloat(ReadOnlyMemory<float> frame, int channelIndex) { return frame.Span[channelIndex]; }

    private static float GetChannelFloat(ReadOnlyMemory<byte> frame, int channelIndex, ushort bitsPerSample)
    {
        var byteIndex = channelIndex * bitsPerSample / 8;

        return SampleSerializer.ReadSample(frame.Span, ref byteIndex, bitsPerSample);
    }
}

using System.Drawing;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.Common;

public enum TextureCoordinate
{
    S = TextureParameterName.TextureWrapS,

    T = TextureParameterName.TextureWrapT,

    R = TextureParameterName.TextureWrapR
}

public unsafe class Texture : IDisposable
{
    public const SizedInternalFormat Srgb8Alpha8 = (SizedInternalFormat)GLEnum.Srgb8Alpha8;

    public const SizedInternalFormat Rgb32F = (SizedInternalFormat)GLEnum.Rgb32f;

    public const GLEnum MaxTextureMaxAnisotropy = (GLEnum)0x84FF;

    public static float? MaxAniso;

    private readonly GL _gl;

    public readonly uint GlTexture;

    public readonly SizedInternalFormat InternalFormat;

    public readonly uint MipmapLevels;

    public readonly uint Width,
                         Height;

    public Texture(GL gl, int width, int height, IntPtr data, bool generateMipmaps = false, bool srgb = false)
    {
        _gl            =   gl;
        MaxAniso       ??= gl.GetFloat(MaxTextureMaxAnisotropy);
        Width          =   (uint)width;
        Height         =   (uint)height;
        InternalFormat =   srgb ? Srgb8Alpha8 : SizedInternalFormat.Rgba8;
        MipmapLevels   =   (uint)(generateMipmaps == false ? 1 : (int)Math.Floor(Math.Log(Math.Max(Width, Height), 2)));

        GlTexture = _gl.GenTexture();
        Bind();

        _gl.TexStorage2D(GLEnum.Texture2D, MipmapLevels, InternalFormat, Width, Height);
        _gl.TexSubImage2D(GLEnum.Texture2D, 0, 0, 0, Width, Height, PixelFormat.Bgra, PixelType.UnsignedByte, (void*)data);

        if ( generateMipmaps )
        {
            _gl.GenerateTextureMipmap(GlTexture);
        }

        SetWrap(TextureCoordinate.S, TextureWrapMode.Repeat);
        SetWrap(TextureCoordinate.T, TextureWrapMode.Repeat);

        _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLevel, MipmapLevels - 1);
    }

    public void Dispose()
    {
        _gl.DeleteTexture(GlTexture);
        _gl.CheckError();
    }

    public void Bind()
    {
        _gl.BindTexture(GLEnum.Texture2D, GlTexture);
        _gl.CheckError();
    }

    public void SetMinFilter(TextureMinFilter filter) { _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinFilter, (int)filter); }

    public void SetMagFilter(TextureMagFilter filter) { _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMagFilter, (int)filter); }

    public void SetAnisotropy(float level)
    {
        const TextureParameterName textureMaxAnisotropy = (TextureParameterName)0x84FE;
        _gl.TexParameter(GLEnum.Texture2D, (GLEnum)textureMaxAnisotropy, GlUtility.Clamp(level, 1, MaxAniso.GetValueOrDefault()));
    }

    public void SetLod(int @base, int min, int max)
    {
        _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, @base);
        _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, min);
        _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, max);
    }

    public void SetWrap(TextureCoordinate coord, TextureWrapMode mode) { _gl.TexParameterI(GLEnum.Texture2D, (TextureParameterName)coord, (int)mode); }

    public void SetData(Rectangle bounds, byte[] data)
    {
        Bind();
        fixed (byte* ptr = data)
        {
            _gl.TexSubImage2D(
                              TextureTarget.Texture2D,
                              0,
                              bounds.Left,
                              bounds.Top,
                              (uint)bounds.Width,
                              (uint)bounds.Height,
                              PixelFormat.Rgba,
                              PixelType.UnsignedByte,
                              ptr
                             );

            _gl.CheckError();
        }
    }
}

using System.Text;
using System.Text.RegularExpressions;

using Microsoft.Extensions.Logging;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Normalizes text for better TTS pronunciation
/// </summary>
public class TextNormalizer : ITextNormalizer
{
    // Whitespace normalization
    private static readonly Regex NonStandardWhitespaceRegex = new(@"[^\S \n]", RegexOptions.Compiled);

    private static readonly Regex MultipleSpacesRegex = new(@" {2,}", RegexOptions.Compiled);

    private static readonly Regex SpaceBeforePunctuationRegex = new(@"\s+([.,;:?!])", RegexOptions.Compiled);

    // Quotation marks
    private static readonly Regex CurlyQuotesRegex = new(@"[\u2018\u2019\u201C\u201D]", RegexOptions.Compiled);

    // Abbreviations
    private static readonly Dictionary<Regex, string> CommonAbbreviations = new() {
                                                                                { new Regex(@"\bDr\.(?=\s+[A-Z])", RegexOptions.Compiled | RegexOptions.IgnoreCase), "Doctor" },
                                                                                { new Regex(@"\bMr\.(?=\s+[A-Z])", RegexOptions.Compiled | RegexOptions.IgnoreCase), "Mister" },
                                                                                { new Regex(@"\bMs\.(?=\s+[A-Z])", RegexOptions.Compiled | RegexOptions.IgnoreCase), "Miss" },
                                                                                { new Regex(@"\bMrs\.(?=\s+[A-Z])", RegexOptions.Compiled | RegexOptions.IgnoreCase), "Missus" },
                                                                                { new Regex(@"\betc\.(?!\s+[A-Z])", RegexOptions.Compiled | RegexOptions.IgnoreCase), "etcetera" },
                                                                                { new Regex(@"\bSt\.(?=\s+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), "Street" },
                                                                                { new Regex(@"\bAve\.(?=\s+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), "Avenue" },
                                                                                { new Regex(@"\bRd\.(?=\s+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), "Road" },
                                                                                { new Regex(@"\bPh\.D\.(?=\s+|\b)", RegexOptions.Compiled | RegexOptions.IgnoreCase), "PhD" }
                                                                                };

    // Number regex for finding numeric patterns
    private static readonly Regex NumberRegex = new(@"(?<!\w)-?\d+(\.\d+)?%?", RegexOptions.Compiled);

    // URLs and email addresses
    private static readonly Regex UrlRegex = new(@"https?://[\w.-]+(?:/[\w.-]*)*/?", RegexOptions.Compiled);

    private static readonly Regex EmailRegex = new(@"[\w.+-]+@[\w.-]+\.\w{2,}", RegexOptions.Compiled);

    private readonly ILogger<TextNormalizer> _logger;

    public TextNormalizer(ILogger<TextNormalizer> logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); }

    /// <summary>
    ///     Normalizes text for TTS synthesis
    /// </summary>
    public string Normalize(string text)
    {
        if ( string.IsNullOrWhiteSpace(text) )
        {
            return string.Empty;
        }

        try
        {
            text = text.Normalize(NormalizationForm.FormC);

            text = CleanupLines(text);

            text = NormalizeCharacters(text);

            text = ProcessUrlsAndEmails(text);

            text = ExpandAbbreviations(text);

            text = NormalizeWhitespaceAndPunctuation(text);

            text = RemoveInvalidSurrogates(text);

            return text.Trim();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during text normalization");

            return text;
        }
    }

    private string CleanupLines(string text)
    {
        // Split by line, trim each line, and remove empty ones
        var lines         = text.Split('\n');
        var nonEmptyLines = new List<string>();

        foreach ( var line in lines )
        {
            var trimmedLine = line.Trim();
            if ( !string.IsNullOrEmpty(trimmedLine) )
            {
                nonEmptyLines.Add(trimmedLine);
            }
        }

        return string.Join("\n", nonEmptyLines);
    }

    private string NormalizeCharacters(string text)
    {
        // Convert curly quotes to straight quotes
        text = CurlyQuotesRegex.Replace(text, match =>
                                              {
                                                  return match.Value switch {
                                                      "\u2018" or "\u2019" => "'",
                                                      "\u201C" or "\u201D" => "\"",
                                                      _ => match.Value
                                                  };
                                              });

        // Replace other special characters
        text = text
               .Replace("…", "...")
               .Replace("–", "-")
               .Replace("—", " - ")
               .Replace("•", "*")
               .Replace("®", " registered ")
               .Replace("©", " copyright ")
               .Replace("™", " trademark ");

        // Convert Unicode fractions to text
        text = text
               .Replace("½", " one half ")
               .Replace("¼", " one quarter ")
               .Replace("¾", " three quarters ");

        return text;
    }

    private string RemoveInvalidSurrogates(string input)
    {
        var sb = new StringBuilder();
        for ( var i = 0; i < input.Length; i++ )
        {
            if ( char.IsHighSurrogate(input[i]) )
            {
                // Check if there's a valid low surrogate following
                if ( i + 1 < input.Length && char.IsLowSurrogate(input[i + 1]) )
                {
                    sb.Append(input[i]);
                    sb.Append(input[i + 1]);
                    i++; // Skip the next character as we've already handled it
                }
                // Otherwise, skip the invalid high surrogate
            }
            else if ( !char.IsLowSurrogate(input[i]) )
            {
                // Keep normal characters, skip orphaned low surrogates
                sb.Append(input[i]);
            }
        }

        return sb.ToString();
    }

    private bool IsValidUtf16(string input)
    {
        for ( var i = 0; i < input.Length; i++ )
        {
            if ( char.IsHighSurrogate(input[i]) )
            {
                if ( i + 1 >= input.Length || !char.IsLowSurrogate(input[i + 1]) )
                {
                    return false;
                }

                i++; // Skip the low surrogate
            }
            else if ( char.IsLowSurrogate(input[i]) )
            {
                return false; // Unexpected low surrogate
            }
        }

        return true;
    }

    private string ProcessUrlsAndEmails(string text)
    {
        // Replace URLs with placeholder text
        text = UrlRegex.Replace(text, match => " URL ");

        // Replace email addresses with readable text
        text = EmailRegex.Replace(text, match =>
                                        {
                                            var parts = match.Value.Split('@');
                                            if ( parts.Length != 2 )
                                            {
                                                return " email address ";
                                            }

                                            var username = string.Join(" ", SplitCamelCase(parts[0]));
                                            var domain   = parts[1].Replace(".", " dot ");

                                            return $" {username} at {domain} ";
                                        });

        return text;
    }

    private IEnumerable<string> SplitCamelCase(string text)
    {
        var buffer = new StringBuilder();

        foreach ( var c in text )
        {
            if ( char.IsUpper(c) && buffer.Length > 0 )
            {
                yield return buffer.ToString();
                buffer.Clear();
            }

            buffer.Append(char.ToLower(c));
        }

        if ( buffer.Length > 0 )
        {
            yield return buffer.ToString();
        }
    }

    private string ExpandAbbreviations(string text)
    {
        foreach ( var kvp in CommonAbbreviations )
        {
            text = kvp.Key.Replace(text, kvp.Value);
        }

        return text;
    }

    private string NormalizeWhitespaceAndPunctuation(string text)
    {
        // Replace non-standard whitespace with normal spaces
        text = NonStandardWhitespaceRegex.Replace(text, " ");

        // Collapse multiple spaces into one
        text = MultipleSpacesRegex.Replace(text, " ");

        // Fix spacing around punctuation
        text = SpaceBeforePunctuationRegex.Replace(text, "$1");

        return text;
    }
}

using System.Text.Json.Nodes;

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

/// <summary>
///     表情のモーションクラス。
/// </summary>
public class CubismExpressionMotion : ACubismMotion
{
    /// <summary>
    ///     加算適用の初期値
    /// </summary>
    public const float DefaultAdditiveValue = 0.0f;

    /// <summary>
    ///     乗算適用の初期値
    /// </summary>
    public const float DefaultMultiplyValue = 1.0f;

    public const string ExpressionKeyFadeIn = "FadeInTime";

    public const string ExpressionKeyFadeOut = "FadeOutTime";

    public const string ExpressionKeyParameters = "Parameters";

    public const string ExpressionKeyId = "Id";

    public const string ExpressionKeyValue = "Value";

    public const string ExpressionKeyBlend = "Blend";

    public const string BlendValueAdd = "Add";

    public const string BlendValueMultiply = "Multiply";

    public const string BlendValueOverwrite = "Overwrite";

    public const float DefaultFadeTime = 1.0f;

    /// <summary>
    ///     インスタンスを作成する。
    /// </summary>
    /// <param name="buffer">expファイルが読み込まれているバッファ</param>
    public CubismExpressionMotion(string buf)
    {
        using var stream = File.Open(buf, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        var       obj    = JsonNode.Parse(stream) ?? throw new Exception("Load ExpressionMotion error");
        var       json   = obj.AsObject();

        FadeInSeconds = json.ContainsKey(ExpressionKeyFadeIn)
                            ? (float)json[ExpressionKeyFadeIn]!
                            : DefaultFadeTime; // フェードイン

        FadeOutSeconds = json.ContainsKey(ExpressionKeyFadeOut)
                             ? (float)json[ExpressionKeyFadeOut]!
                             : DefaultFadeTime; // フェードアウト

        if ( FadeInSeconds < 0.0f )
        {
            FadeInSeconds = DefaultFadeTime;
        }

        if ( FadeOutSeconds < 0.0f )
        {
            FadeOutSeconds = DefaultFadeTime;
        }

        // 各パラメータについて
        var list           = json[ExpressionKeyParameters]!;
        var parameterCount = list.AsArray().Count;

        for ( var i = 0; i < parameterCount; ++i )
        {
            var param       = list[i]!;
            var parameterId = CubismFramework.CubismIdManager.GetId(param[ExpressionKeyId]!.ToString()); // パラメータID
            var value       = (float)param[ExpressionKeyValue]!;                                         // 値

            // 計算方法の設定
            ExpressionBlendType blendType;
            var                 type = param[ExpressionKeyBlend]?.ToString();
            if ( type == null || type == BlendValueAdd )
            {
                blendType = ExpressionBlendType.Add;
            }
            else if ( type == BlendValueMultiply )
            {
                blendType = ExpressionBlendType.Multiply;
            }
            else if ( type == BlendValueOverwrite )
            {
                blendType = ExpressionBlendType.Overwrite;
            }
            else
            {
                // その他 仕様にない値を設定したときは加算モードにすることで復旧
                blendType = ExpressionBlendType.Add;
            }

            // 設定オブジェクトを作成してリストに追加する
            Parameters.Add(new ExpressionParameter { ParameterId = parameterId, BlendType = blendType, Value = value });
        }
    }

    /// <summary>
    ///     表情のパラメータ情報リスト
    /// </summary>
    public List<ExpressionParameter> Parameters { get; init; } = [];

    /// <summary>
    ///     表情のフェードのウェイト値
    /// </summary>
    [Obsolete("CubismExpressionMotion._fadeWeightが削除予定のため非推奨\nCubismExpressionMotionManager.getFadeWeight(int index) を使用してください。")]
    public float FadeWeight { get; private set; }

    public override void DoUpdateParameters(CubismModel model, float userTimeSeconds, float weight, CubismMotionQueueEntry motionQueueEntry)
    {
        foreach ( var item in Parameters )
        {
            switch ( item.BlendType )
            {
                case ExpressionBlendType.Add:
                {
                    model.AddParameterValue(item.ParameterId, item.Value, weight); // 相対変化 加算

                    break;
                }
                case ExpressionBlendType.Multiply:
                {
                    model.MultiplyParameterValue(item.ParameterId, item.Value, weight); // 相対変化 乗算

                    break;
                }
                case ExpressionBlendType.Overwrite:
                {
                    model.SetParameterValue(item.ParameterId, item.Value, weight); // 絶対変化 上書き

                    break;
                }
            }
        }
    }

    /// <summary>
    ///     モデルの表情に関するパラメータを計算する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="userTimeSeconds">対象のモデル</param>
    /// <param name="motionQueueEntry">CubismMotionQueueManagerで管理されているモーション</param>
    /// <param name="expressionParameterValues">モデルに適用する各パラメータの値</param>
    /// <param name="expressionIndex">表情のインデックス</param>
    public void CalculateExpressionParameters(CubismModel                     model,                     float userTimeSeconds, CubismMotionQueueEntry? motionQueueEntry,
                                              List<ExpressionParameterValue>? expressionParameterValues, int   expressionIndex, float                   fadeWeight)
    {
        if ( motionQueueEntry == null || expressionParameterValues == null )
        {
            return;
        }

        if ( !motionQueueEntry.Available )
        {
            return;
        }

        // CubismExpressionMotion._fadeWeight は廃止予定です。
        // 互換性のために処理は残りますが、実際には使用しておりません。
        FadeWeight = UpdateFadeWeight(motionQueueEntry, userTimeSeconds);

        // モデルに適用する値を計算
        for ( var i = 0; i < expressionParameterValues.Count; ++i )
        {
            var expressionParameterValue = expressionParameterValues[i];

            if ( expressionParameterValue.ParameterId == null )
            {
                continue;
            }

            var currentParameterValue = expressionParameterValue.OverwriteValue =
                                            model.GetParameterValue(expressionParameterValue.ParameterId);

            var expressionParameters = Parameters;
            var parameterIndex       = -1;
            for ( var j = 0; j < expressionParameters.Count; ++j )
            {
                if ( expressionParameterValue.ParameterId != expressionParameters[j].ParameterId )
                {
                    continue;
                }

                parameterIndex = j;

                break;
            }

            // 再生中のExpressionが参照していないパラメータは初期値を適用
            if ( parameterIndex < 0 )
            {
                if ( expressionIndex == 0 )
                {
                    expressionParameterValues[i].AdditiveValue = DefaultAdditiveValue;

                    expressionParameterValues[i].MultiplyValue = DefaultMultiplyValue;

                    expressionParameterValues[i].OverwriteValue = currentParameterValue;
                }
                else
                {
                    expressionParameterValues[i].AdditiveValue =
                        CalculateValue(expressionParameterValue.AdditiveValue, DefaultAdditiveValue, fadeWeight);

                    expressionParameterValues[i].MultiplyValue =
                        CalculateValue(expressionParameterValue.MultiplyValue, DefaultMultiplyValue, fadeWeight);

                    expressionParameterValues[i].OverwriteValue =
                        CalculateValue(expressionParameterValue.OverwriteValue, currentParameterValue, fadeWeight);
                }

                continue;
            }

            // 値を計算
            var value = expressionParameters[parameterIndex].Value;
            float newAdditiveValue,
                  newMultiplyValue,
                  newSetValue;

            switch ( expressionParameters[parameterIndex].BlendType )
            {
                case ExpressionBlendType.Add:
                    newAdditiveValue = value;
                    newMultiplyValue = DefaultMultiplyValue;
                    newSetValue      = currentParameterValue;

                    break;
                case ExpressionBlendType.Multiply:
                    newAdditiveValue = DefaultAdditiveValue;
                    newMultiplyValue = value;
                    newSetValue      = currentParameterValue;

                    break;
                case ExpressionBlendType.Overwrite:
                    newAdditiveValue = DefaultAdditiveValue;
                    newMultiplyValue = DefaultMultiplyValue;
                    newSetValue      = value;

                    break;
                default:
                    return;
            }

            if ( expressionIndex == 0 )
            {
                expressionParameterValues[i].AdditiveValue  = newAdditiveValue;
                expressionParameterValues[i].MultiplyValue  = newMultiplyValue;
                expressionParameterValues[i].OverwriteValue = newSetValue;
            }
            else
            {
                expressionParameterValues[i].AdditiveValue  = expressionParameterValue.AdditiveValue * (1.0f - FadeWeight) + newAdditiveValue * FadeWeight;
                expressionParameterValues[i].MultiplyValue  = expressionParameterValue.MultiplyValue * (1.0f - FadeWeight) + newMultiplyValue * FadeWeight;
                expressionParameterValues[i].OverwriteValue = expressionParameterValue.OverwriteValue * (1.0f - FadeWeight) + newSetValue * FadeWeight;
            }
        }
    }

    private float CalculateValue(float source, float destination, float fadeWeight) { return source * (1.0f - fadeWeight) + destination * fadeWeight; }
}

//IDで指定された目のパラメータが、0のときに閉じるなら true 、1の時に閉じるなら false 。
//#define CloseIfZero

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Effect;

/// <summary>
///     自動まばたき機能を提供する。
/// </summary>
public class CubismEyeBlink
{
    private readonly Random _random = new();

    /// <summary>
    ///     操作対象のパラメータのIDのリスト
    /// </summary>
    public readonly List<string> ParameterIds = [];

    /// <summary>
    ///     まばたきの間隔[秒]
    /// </summary>
    private float _blinkingIntervalSeconds;

    /// <summary>
    ///     現在の状態
    /// </summary>
    private EyeState _blinkingState;

    /// <summary>
    ///     まぶたを閉じている動作の所要時間[秒]
    /// </summary>
    private float _closedSeconds;

    /// <summary>
    ///     まぶたを閉じる動作の所要時間[秒]
    /// </summary>
    private float _closingSeconds;

    /// <summary>
    ///     次のまばたきの時刻[秒]
    /// </summary>
    private float _nextBlinkingTime;

    /// <summary>
    ///     まぶたを開く動作の所要時間[秒]
    /// </summary>
    private float _openingSeconds;

    /// <summary>
    ///     現在の状態が開始した時刻[秒]
    /// </summary>
    private float _stateStartTimeSeconds;

    /// <summary>
    ///     デルタ時間の積算値[秒]
    /// </summary>
    private float _userTimeSeconds;

    /// <summary>
    ///     インスタンスを作成する。
    /// </summary>
    /// <param name="modelSetting">モデルの設定情報</param>
    public CubismEyeBlink(ModelSettingObj modelSetting)
    {
        _blinkingState           = EyeState.First;
        _blinkingIntervalSeconds = 4.0f;
        _closingSeconds          = 0.1f;
        _closedSeconds           = 0.05f;
        _openingSeconds          = 0.15f;

        foreach ( var item in modelSetting.Groups )
        {
            if ( item.Name == CubismModelSettingJson.EyeBlink )
            {
                foreach ( var item1 in item.Ids )
                {
                    if ( item1 == null )
                    {
                        continue;
                    }

                    var item2 = CubismFramework.CubismIdManager.GetId(item1);
                    ParameterIds.Add(item2);
                }

                break;
            }
        }
    }

    /// <summary>
    ///     まばたきの間隔を設定する。
    /// </summary>
    /// <param name="blinkingInterval">まばたきの間隔の時間[秒]</param>
    public void SetBlinkingInterval(float blinkingInterval) { _blinkingIntervalSeconds = blinkingInterval; }

    /// <summary>
    ///     まばたきのモーションの詳細設定を行う。
    /// </summary>
    /// <param name="closing">まぶたを閉じる動作の所要時間[秒]</param>
    /// <param name="closed">まぶたを閉じている動作の所要時間[秒]</param>
    /// <param name="opening">まぶたを開く動作の所要時間[秒]</param>
    public void SetBlinkingSettings(float closing, float closed, float opening)
    {
        _closingSeconds = closing;
        _closedSeconds  = closed;
        _openingSeconds = opening;
    }

    /// <summary>
    ///     モデルのパラメータを更新する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="deltaTimeSeconds">デルタ時間[秒]</param>
    public void UpdateParameters(CubismModel model, float deltaTimeSeconds)
    {
        _userTimeSeconds += deltaTimeSeconds;
        float parameterValue;
        float t;
        switch ( _blinkingState )
        {
            case EyeState.Closing:
                t = (_userTimeSeconds - _stateStartTimeSeconds) / _closingSeconds;

                if ( t >= 1.0f )
                {
                    t                      = 1.0f;
                    _blinkingState         = EyeState.Closed;
                    _stateStartTimeSeconds = _userTimeSeconds;
                }

                parameterValue = 1.0f - t;

                break;
            case EyeState.Closed:
                t = (_userTimeSeconds - _stateStartTimeSeconds) / _closedSeconds;

                if ( t >= 1.0f )
                {
                    _blinkingState         = EyeState.Opening;
                    _stateStartTimeSeconds = _userTimeSeconds;
                }

                parameterValue = 0.0f;

                break;
            case EyeState.Opening:
                t = (_userTimeSeconds - _stateStartTimeSeconds) / _openingSeconds;

                if ( t >= 1.0f )
                {
                    t                 = 1.0f;
                    _blinkingState    = EyeState.Interval;
                    _nextBlinkingTime = DetermineNextBlinkingTiming();
                }

                parameterValue = t;

                break;
            case EyeState.Interval:
                if ( _nextBlinkingTime < _userTimeSeconds )
                {
                    _blinkingState         = EyeState.Closing;
                    _stateStartTimeSeconds = _userTimeSeconds;
                }

                parameterValue = 1.0f;

                break;
            case EyeState.First:
            default:
                _blinkingState    = EyeState.Interval;
                _nextBlinkingTime = DetermineNextBlinkingTiming();

                parameterValue = 1.0f;

                break;
        }
#if CloseIfZero
        parameterValue = -parameterValue;
#endif

        foreach ( var item in ParameterIds )
        {
            model.SetParameterValue(item, parameterValue);
        }
    }

    /// <summary>
    ///     次のまばたきのタイミングを決定する。
    /// </summary>
    /// <returns>次のまばたきを行う時刻[秒]</returns>
    private float DetermineNextBlinkingTiming()
    {
        float r = _random.Next() / int.MaxValue;

        return _userTimeSeconds + r * (2.0f * _blinkingIntervalSeconds - 1.0f);
    }
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents an audio source that can be awaited for audio data.
/// </summary>
/// <remarks>
///     Important: This should be used at most by one writer and one reader.
///     It can store samples as floats or as bytes, or both. By default, it stores samples as floats.
///     Based on your usage, you can choose to store samples as bytes, floats, or both.
///     If storing them as floats, they will be deserialized from bytes when they are added and returned directly when
///     requested.
///     If storing them as bytes, they will be serialized from floats when they are added and returned directly when
///     requested.
///     If you want to optimize your memory usage, you can store them in the same format as they are added.
///     If you want to optimize your CPU usage, you can store them in the format you want to use them in.
/// </remarks>
public class AwaitableAudioSource(
    IReadOnlyDictionary<string, string> metadata,
    bool                                storeSamples        = true,
    bool                                storeBytes          = false,
    int                                 initialSizeFloats   = BufferedMemoryAudioSource.DefaultInitialSize,
    int                                 initialSizeBytes    = BufferedMemoryAudioSource.DefaultInitialSize,
    IChannelAggregationStrategy?        aggregationStrategy = null)
    : DiscardableMemoryAudioSource(metadata, storeSamples, storeBytes, initialSizeFloats, initialSizeBytes, aggregationStrategy), IAwaitableAudioSource
{
    private readonly TaskCompletionSource<bool> initializationTcs = new();

    private readonly AsyncAutoResetEvent samplesAvailableEvent = new();

    protected readonly Lock syncRoot = new();

    /// <summary>
    ///     Gets a value indicating whether the source is flushed.
    /// </summary>
    public bool IsFlushed { get; private set; }

    public override Task<Memory<byte>> GetFramesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        lock (syncRoot)
        {
            // Calling the base method with lock is fine here, as the base method will not await anything.
            return base.GetFramesAsync(startFrame, maxFrames, cancellationToken);
        }
    }

    public override Task<int> CopyFramesAsync(Memory<byte> destination, long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        lock (syncRoot)
        {
            // Calling the base method with lock is fine here, as the base method will not await anything.
            return base.CopyFramesAsync(destination, startFrame, maxFrames, cancellationToken);
        }
    }

    public override Task<Memory<float>> GetSamplesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default)
    {
        lock (syncRoot)
        {
            // Calling the base method with lock is fine here, as the base method will not await anything.
            return base.GetSamplesAsync(startFrame, maxFrames, cancellationToken);
        }
    }

    /// <inheritdoc />
    public async Task WaitForNewSamplesAsync(long sampleCount, CancellationToken cancellationToken)
    {
        while ( !IsFlushed && SampleVirtualCount <= sampleCount )
        {
            await samplesAvailableEvent.WaitAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
        }
    }

    public async Task WaitForNewSamplesAsync(TimeSpan minimumDuration, CancellationToken cancellationToken)
    {
        while ( !IsFlushed && Duration <= minimumDuration )
        {
            await samplesAvailableEvent.WaitAsync().WaitAsync(cancellationToken).ConfigureAwait(false);
        }
    }

    /// <inheritdoc />
    public async Task WaitForInitializationAsync(CancellationToken cancellationToken)
    {
        if ( IsInitialized )
        {
            return;
        }

        await initializationTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public void Flush()
    {
        lock (syncRoot)
        {
            IsFlushed = true;
            samplesAvailableEvent.Set();
        }
    }

    public override void DiscardFrames(int count)
    {
        lock (syncRoot)
        {
            base.DiscardFrames(count);
        }
    }

    public override void AddFrame(ReadOnlyMemory<byte> frame)
    {
        lock (syncRoot)
        {
            if ( IsFlushed )
            {
                throw new InvalidOperationException("The source is flushed and cannot accept new frames.");
            }

            base.AddFrame(frame);
            if ( IsInitialized && !initializationTcs.Task.IsCompleted )
            {
                initializationTcs.SetResult(true);
            }
        }
    }

    public override void AddFrame(ReadOnlyMemory<float> frame)
    {
        lock (syncRoot)
        {
            if ( IsFlushed )
            {
                throw new InvalidOperationException("The source is flushed and cannot accept new frames.");
            }

            base.AddFrame(frame);
            if ( IsInitialized && !initializationTcs.Task.IsCompleted )
            {
                initializationTcs.SetResult(true);
            }
        }
    }

    /// <summary>
    ///     Notifies that new samples are available.
    /// </summary>
    public void NotifyNewSamples() { samplesAvailableEvent.Set(); }
}

using System.Numerics;

namespace PersonaEngine.Lib.UI.RouletteWheel;

public partial class RouletteWheel
{
    /// <summary>
    ///     Defines how the wheel position is tracked and updated during window resizing.
    /// </summary>
    public enum PositionMode
    {
        /// <summary>
        ///     Position is fixed in absolute pixels and won't change during resizing.
        /// </summary>
        Absolute,

        /// <summary>
        ///     Position is based on viewport percentages and will scale with resizing.
        /// </summary>
        Percentage,

        /// <summary>
        ///     Position is based on viewport anchors and will adapt during resizing.
        /// </summary>
        Anchored
    }

    /// <summary>
    ///     Represents anchor points within the viewport for positioning the wheel.
    /// </summary>
    public enum ViewportAnchor
    {
        Center,

        TopLeft,

        TopCenter,

        TopRight,

        MiddleLeft,

        MiddleRight,

        BottomLeft,

        BottomCenter,

        BottomRight
    }

    private Vector2 _anchorOffset = Vector2.Zero;

    private ViewportAnchor _currentAnchor = ViewportAnchor.Center;

    private PositionMode _positionMode = PositionMode.Absolute;

    private Vector2 _positionPercentage = new(0.5f, 0.5f);

    /// <summary>
    ///     Gets the current radius of the wheel in pixels.
    /// </summary>
    public float Radius => Diameter / 2;

    /// <summary>
    ///     Gets the current diameter of the wheel in pixels.
    /// </summary>
    public float Diameter => Math.Min(_viewportWidth, _viewportHeight) * _wheelSize * OUTER_RADIUS_FACTOR;

    /// <summary>
    ///     Gets or sets the rotation of the wheel in radians.
    /// </summary>
    public float Rotation { get; private set; } = 0f;

    /// <summary>
    ///     Gets or sets the rotation of the wheel in degrees.
    /// </summary>
    public float RotationDegrees => Rotation * 180 / MathF.PI;

    public void Resize()
    {
        _viewportWidth  = _config.CurrentValue.Width;
        _viewportHeight = _config.CurrentValue.Height;
        _textRenderer.OnViewportChanged(_viewportWidth, _viewportHeight);

        switch ( _positionMode )
        {
            case PositionMode.Anchored:
                UpdatePositionFromAnchor();

                break;
            case PositionMode.Percentage:
                UpdatePositionFromPercentage();

                break;
            case PositionMode.Absolute:
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        if ( _useAdaptiveTextSize )
        {
            CalculateAllSegmentFontSizes();
        }
    }

    /// <summary>
    ///     Rotates the wheel by the specified angle in radians.
    /// </summary>
    public void RotateBy(float angleRadians) { Rotation += angleRadians; }

    /// <summary>
    ///     Rotates the wheel by the specified angle in radians.
    /// </summary>
    public void RotateByDegrees(float angleDegrees) { Rotation += angleDegrees * MathF.PI / 180; }

    /// <summary>
    ///     Sets the diameter of the wheel in pixels.
    /// </summary>
    public void SetDiameter(float diameter)
    {
        float minDimension = Math.Min(_viewportWidth, _viewportHeight);
        _wheelSize = Math.Clamp(diameter / minDimension, 0.1f, 1.0f);
    }

    /// <summary>
    ///     Sets the radius of the wheel in pixels.
    /// </summary>
    public void SetRadius(float radius) { SetDiameter(radius * 2); }

    /// <summary>
    ///     Sets the wheel size as a percentage of the viewport (0.0 to 1.0).
    /// </summary>
    /// <param name="percentage">Value between 0.0 and 1.0</param>
    public void SetSizePercentage(float percentage) { _wheelSize = Math.Clamp(percentage, 0.1f, 1.0f); }

    /// <summary>
    ///     Scales the wheel size by the specified factor relative to its current size.
    /// </summary>
    public void Scale(float factor) { _wheelSize = Math.Clamp(_wheelSize * factor, 0.1f, 1.0f); }

    /// <summary>
    ///     Translates the wheel by the specified amount.
    /// </summary>
    public void Translate(float deltaX, float deltaY)
    {
        _positionMode = PositionMode.Absolute;
        _position     = new Vector2(_position.X + deltaX, _position.Y + deltaY);
    }

    /// <summary>
    ///     Translates the wheel by the specified vector.
    /// </summary>
    public void Translate(Vector2 delta)
    {
        _positionMode =  PositionMode.Absolute;
        _position     += delta;
    }

    /// <summary>
    ///     Sets the position using viewport percentages (0.0 to 1.0 for each axis)
    /// </summary>
    public void PositionByPercentage(float xPercent, float yPercent)
    {
        _positionMode = PositionMode.Percentage;
        _positionPercentage = new Vector2(
                                          Math.Clamp(xPercent, 0, 1),
                                          Math.Clamp(yPercent, 0, 1)
                                         );

        UpdatePositionFromPercentage();
    }

    /// <summary>
    ///     Updates position based on viewport percentages
    /// </summary>
    private void UpdatePositionFromPercentage()
    {
        _position = new Vector2(
                                _viewportWidth * _positionPercentage.X,
                                _viewportHeight * _positionPercentage.Y
                               );
    }

    /// <summary>
    ///     Sets the position in absolute pixels. This will not adjust with window resizing.
    /// </summary>
    public void SetAbsolutePosition(float x, float y)
    {
        _positionMode = PositionMode.Absolute;
        _position     = new Vector2(x, y);
    }

    /// <summary>
    ///     Positions the wheel at the specified viewport anchor with an optional offset.
    /// </summary>
    public void PositionAt(ViewportAnchor anchor, Vector2 offset = default)
    {
        _positionMode  = PositionMode.Anchored;
        _currentAnchor = anchor;
        _anchorOffset  = offset;

        UpdatePositionFromAnchor();
    }

    /// <summary>
    ///     Updates position based on the current anchor and offset
    /// </summary>
    private void UpdatePositionFromAnchor()
    {
        float x = 0,
              y = 0;

        switch ( _currentAnchor )
        {
            case ViewportAnchor.Center:
                x = _viewportWidth / 2.0f;
                y = _viewportHeight / 2.0f;

                break;
            case ViewportAnchor.TopLeft:
                x = Radius;
                y = Radius;

                break;
            case ViewportAnchor.TopCenter:
                x = _viewportWidth / 2.0f;
                y = Radius;

                break;
            case ViewportAnchor.TopRight:
                x = _viewportWidth - Radius;
                y = Radius;

                break;
            case ViewportAnchor.MiddleLeft:
                x = Radius;
                y = _viewportHeight / 2.0f;

                break;
            case ViewportAnchor.MiddleRight:
                x = _viewportWidth - Radius;
                y = _viewportHeight / 2.0f;

                break;
            case ViewportAnchor.BottomLeft:
                x = Radius;
                y = _viewportHeight - Radius;

                break;
            case ViewportAnchor.BottomCenter:
                x = _viewportWidth / 2.0f;
                y = _viewportHeight - Radius;

                break;
            case ViewportAnchor.BottomRight:
                x = _viewportWidth - Radius;
                y = _viewportHeight - Radius;

                break;
        }

        _position = new Vector2(x + _anchorOffset.X, y + _anchorOffset.Y);
    }

    /// <summary>
    ///     Centers the wheel in the viewport.
    /// </summary>
    public void CenterInViewport() { _position = new Vector2(_viewportWidth / 2.0f, _viewportHeight / 2.0f); }
}

using System.Runtime.InteropServices;

using PersonaEngine.Lib.Live2D.Framework.Core;

namespace PersonaEngine.Lib.Live2D.Framework.Model;

/// <summary>
///     Mocデータの管理を行うクラス。
/// </summary>
public class CubismMoc : IDisposable
{
    /// <summary>
    ///     Mocデータ
    /// </summary>
    private readonly IntPtr _moc;

    /// <summary>
    ///     バッファからMocファイルを読み取り、Mocデータを作成する。
    /// </summary>
    /// <param name="mocBytes"> Mocファイルのバッファ</param>
    /// <param name="shouldCheckMocConsistency">MOCの整合性チェックフラグ(初期値 : false)</param>
    /// <returns></returns>
    public CubismMoc(byte[] mocBytes, bool shouldCheckMocConsistency = false)
    {
        var alignedBuffer = CubismFramework.AllocateAligned(mocBytes.Length, CsmEnum.csmAlignofMoc);
        Marshal.Copy(mocBytes, 0, alignedBuffer, mocBytes.Length);

        if ( shouldCheckMocConsistency )
        {
            // .moc3の整合性を確認
            var consistency = HasMocConsistency(alignedBuffer, mocBytes.Length);
            if ( !consistency )
            {
                CubismFramework.DeallocateAligned(alignedBuffer);

                // 整合性が確認できなければ処理しない
                throw new Exception("Inconsistent MOC3.");
            }
        }

        var moc = CubismCore.ReviveMocInPlace(alignedBuffer, mocBytes.Length);

        if ( moc == IntPtr.Zero )
        {
            throw new Exception("MOC3 is null");
        }

        _moc = moc;

        MocVersion = CubismCore.GetMocVersion(alignedBuffer, mocBytes.Length);

        var modelSize   = CubismCore.GetSizeofModel(_moc);
        var modelMemory = CubismFramework.AllocateAligned(modelSize, CsmEnum.CsmAlignofModel);

        var model = CubismCore.InitializeModelInPlace(_moc, modelMemory, modelSize);

        if ( model == IntPtr.Zero )
        {
            throw new Exception("MODEL is null");
        }

        Model = new CubismModel(model);
    }

    /// <summary>
    ///     読み込んだモデルの.moc3 Version
    /// </summary>
    public uint MocVersion { get; }

    public CubismModel Model { get; }

    /// <summary>
    ///     デストラクタ。
    /// </summary>
    public void Dispose()
    {
        Model.Dispose();
        CubismFramework.DeallocateAligned(_moc);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    ///     最新の.moc3 Versionを取得する。
    /// </summary>
    /// <returns></returns>
    public static uint GetLatestMocVersion() { return CubismCore.GetLatestMocVersion(); }

    /// <summary>
    ///     Checks consistency of a moc.
    /// </summary>
    /// <param name="address">Address of unrevived moc. The address must be aligned to 'csmAlignofMoc'.</param>
    /// <param name="size">Size of moc (in bytes).</param>
    /// <returns>'1' if Moc is valid; '0' otherwise.</returns>
    public static bool HasMocConsistency(IntPtr address, int size) { return CubismCore.HasMocConsistency(address, size); }

    /// <summary>
    ///     Checks consistency of a moc.
    /// </summary>
    /// <param name="mocBytes">Mocファイルのバッファ</param>
    /// <param name="size">バッファのサイズ</param>
    /// <returns>'true' if Moc is valid; 'false' otherwise.</returns>
    public static bool HasMocConsistencyFromUnrevivedMoc(byte[] data)
    {
        var alignedBuffer = CubismFramework.AllocateAligned(data.Length, CsmEnum.csmAlignofMoc);
        Marshal.Copy(data, 0, alignedBuffer, data.Length);

        var consistency = HasMocConsistency(alignedBuffer, data.Length);

        CubismFramework.DeallocateAligned(alignedBuffer);

        return consistency;
    }
}

using System.Runtime.InteropServices;

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     画像読み込み、管理を行うクラス。
/// </summary>
public class LAppTextureManager(LAppDelegate lapp)
{
    private readonly List<TextureInfo> _textures = [];

    /// <summary>
    ///     画像読み込み
    /// </summary>
    /// <param name="fileName">読み込む画像ファイルパス名</param>
    /// <returns>画像情報。読み込み失敗時はNULLを返す</returns>
    public TextureInfo CreateTextureFromPngFile(string fileName)
    {
        //search loaded texture already.
        var item = _textures.FirstOrDefault(a => a.FileName == fileName);
        if ( item != null )
        {
            return item;
        }

        // Using SixLabors.ImageSharp to load the image
        using var image = Image.Load<Rgba32>(fileName);
        var       GL    = lapp.GL;

        // OpenGL用のテクスチャを生成する
        var textureId = GL.GenTexture();
        GL.BindTexture(GL.GL_TEXTURE_2D, textureId);

        // Create a byte array to hold image data
        var pixelData = new byte[4 * image.Width * image.Height]; // 4 bytes per pixel (RGBA)

        // Access the pixel data
        image.ProcessPixelRows(accessor =>
                               {
                                   // For each row
                                   for ( var y = 0; y < accessor.Height; y++ )
                                   {
                                       // Get the pixel row span
                                       var row = accessor.GetRowSpan(y);

                                       // For each pixel in the row
                                       for ( var x = 0; x < row.Length; x++ )
                                       {
                                           // Calculate the position in our byte array
                                           var arrayPos = (y * accessor.Width + x) * 4;

                                           // Copy RGBA components
                                           pixelData[arrayPos + 0] = row[x].R;
                                           pixelData[arrayPos + 1] = row[x].G;
                                           pixelData[arrayPos + 2] = row[x].B;
                                           pixelData[arrayPos + 3] = row[x].A;
                                       }
                                   }
                               });

        // Pin the byte array in memory so we can pass a pointer to OpenGL
        var pinnedArray = GCHandle.Alloc(pixelData, GCHandleType.Pinned);
        try
        {
            // Get the address of the first byte in the array
            var pointer = pinnedArray.AddrOfPinnedObject();

            // Pass the pointer to OpenGL
            GL.TexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, image.Width, image.Height, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pointer);
            GL.GenerateMipmap(GL.GL_TEXTURE_2D);
            GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_LINEAR);
            GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
        }
        finally
        {
            // Free the pinned array
            pinnedArray.Free();
        }

        GL.BindTexture(GL.GL_TEXTURE_2D, 0);

        var info = new TextureInfo { FileName = fileName, Width = image.Width, Height = image.Height, ID = textureId };

        _textures.Add(info);

        return info;
    }

    /// <summary>
    ///     指定したテクスチャIDの画像を解放する
    /// </summary>
    /// <param name="textureId">解放するテクスチャID</param>
    public void ReleaseTexture(int textureId)
    {
        foreach ( var item in _textures )
        {
            if ( item.ID == textureId )
            {
                _textures.Remove(item);

                break;
            }
        }
    }

    /// <summary>
    ///     テクスチャIDからテクスチャ情報を得る
    /// </summary>
    /// <param name="textureId">取得したいテクスチャID</param>
    /// <returns>テクスチャが存在していればTextureInfoが返る</returns>
    public TextureInfo? GetTextureInfoById(int textureId) { return _textures.FirstOrDefault(a => a.ID == textureId); }
}

using PersonaEngine.Lib.Live2D.Framework;
using PersonaEngine.Lib.Live2D.Framework.Core;
using PersonaEngine.Lib.Live2D.Framework.Rendering;
using PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     アプリケーションクラス。
///     Cubism SDK の管理を行う。
/// </summary>
public class LAppDelegate : IDisposable
{
    /// <summary>
    ///     Cubism SDK Allocator
    /// </summary>
    private readonly LAppAllocator _cubismAllocator;

    /// <summary>
    ///     Cubism SDK Option
    /// </summary>
    private readonly Option _cubismOption;

    /// <summary>
    ///     クリックしているか
    /// </summary>
    private bool _captured;

    /// <summary>
    ///     マウスX座標
    /// </summary>
    private float _mouseX;

    /// <summary>
    ///     マウスY座標
    /// </summary>
    private float _mouseY;

    /// <summary>
    ///     Initialize関数で設定したウィンドウ高さ
    /// </summary>
    private int _windowHeight;

    /// <summary>
    ///     Initialize関数で設定したウィンドウ幅
    /// </summary>
    private int _windowWidth;

    public LAppDelegate(OpenGLApi gl, LogFunction log)
    {
        GL = gl;

        View             = new LAppView(this);
        TextureManager   = new LAppTextureManager(this);
        _cubismAllocator = new LAppAllocator();

        //テクスチャサンプリング設定
        GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
        GL.TexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);

        //透過設定
        GL.Enable(GL.GL_BLEND);
        GL.BlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);

        // ウィンドウサイズ記憶
        GL.GetWindowSize(out _windowWidth, out _windowHeight);

        //AppViewの初期化
        View.Initialize();

        // Cubism SDK の初期化
        _cubismOption = new Option { LogFunction = log, LoggingLevel = LAppDefine.CubismLoggingLevel };
        CubismFramework.StartUp(_cubismAllocator, _cubismOption);

        //Initialize cubism
        CubismFramework.Initialize();

        //load model
        Live2dManager = new LAppLive2DManager(this);

        LAppPal.DeltaTime = 0;
    }

    /// <summary>
    ///     テクスチャマネージャー
    /// </summary>
    public LAppTextureManager TextureManager { get; private set; }

    public LAppLive2DManager Live2dManager { get; private set; }

    public OpenGLApi GL { get; }

    /// <summary>
    ///     View情報
    /// </summary>
    public LAppView View { get; private set; }

    public CubismTextureColor BGColor { get; set; } = new(0, 0, 0, 0);

    /// <summary>
    ///     解放する。
    /// </summary>
    public void Dispose()
    {
        // リソースを解放
        Live2dManager.Dispose();

        //Cubism SDK の解放
        CubismFramework.Dispose();

        GC.SuppressFinalize(this);
    }

    public void Resize()
    {
        GL.GetWindowSize(out var width, out var height);
        if ( (_windowWidth != width || _windowHeight != height) && width > 0 && height > 0 )
        {
            //AppViewの初期化
            View.Initialize();
            // サイズを保存しておく
            _windowWidth  = width;
            _windowHeight = height;
        }
    }

    public void Update(float tick)
    {
        // 時間更新
        LAppPal.DeltaTime = tick;
    }

    /// <summary>
    ///     実行処理。
    /// </summary>
    public void Run()
    {
        // 画面の初期化
        GL.ClearColor(BGColor.R, BGColor.G, BGColor.B, BGColor.A);
        GL.Clear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
        GL.ClearDepthf(1.0f);

        //描画更新
        View.Render();
    }

    /// <summary>
    ///     OpenGL用 glfwSetMouseButtonCallback用関数。
    /// </summary>
    /// <param name="button">ボタン種類</param>
    /// <param name="action">実行結果</param>
    public void OnMouseCallBack(bool press)
    {
        if ( press )
        {
            _captured = true;
            View.OnTouchesBegan(_mouseX, _mouseY);
        }
        else
        {
            if ( _captured )
            {
                _captured = false;
                View.OnTouchesEnded(_mouseX, _mouseY);
            }
        }
    }

    /// <summary>
    ///     OpenGL用 glfwSetCursorPosCallback用関数。
    /// </summary>
    /// <param name="x">x座標</param>
    /// <param name="y">x座標</param>
    public void OnMouseCallBack(float x, float y)
    {
        if ( !_captured )
        {
            return;
        }

        _mouseX = x;
        _mouseY = y;

        View.OnTouchesMoved(_mouseX, _mouseY);
    }

    public void StartSpeaking(string filePath) { Live2dManager.StartSpeaking(filePath); }
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

/// <summary>
///     モーション再生の管理用クラス。CubismMotionモーションなどACubismMotionのサブクラスを再生するために使用する。
///     再生中に別のモーションが StartMotion()された場合は、新しいモーションに滑らかに変化し旧モーションは中断する。
///     表情用モーション、体用モーションなどを分けてモーション化した場合など、
///     複数のモーションを同時に再生させる場合は、複数のCubismMotionQueueManagerインスタンスを使用する。
/// </summary>
public class CubismMotionQueueManager
{
    private readonly List<CubismMotionQueueEntry> _remove = [];

    /// <summary>
    ///     モーション
    /// </summary>
    protected readonly List<CubismMotionQueueEntry> Motions = [];

    /// <summary>
    ///     コールバック関数ポインタ
    /// </summary>
    private CubismMotionEventFunction? _eventCallback;

    /// <summary>
    ///     コールバックに戻されるデータ
    /// </summary>
    private CubismUserModel? _eventCustomData;

    /// <summary>
    ///     デルタ時間の積算値[秒]
    /// </summary>
    protected float UserTimeSeconds;

    /// <summary>
    ///     指定したモーションを開始する。同じタイプのモーションが既にある場合は、既存のモーションに終了フラグを立て、フェードアウトを開始させる。
    /// </summary>
    /// <param name="motion">開始するモーション</param>
    /// <returns>開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」</returns>
    public CubismMotionQueueEntry StartMotion(ACubismMotion motion)
    {
        CubismMotionQueueEntry motionQueueEntry;

        // 既にモーションがあれば終了フラグを立てる
        for ( var i = 0; i < Motions.Count; ++i )
        {
            motionQueueEntry = Motions[0];
            if ( motionQueueEntry == null )
            {
                continue;
            }

            motionQueueEntry.SetFadeout(motionQueueEntry.Motion.FadeOutSeconds);
        }

        motionQueueEntry = new CubismMotionQueueEntry { Motion = motion };

        Motions.Add(motionQueueEntry);

        return motionQueueEntry;
    }

    /// <summary>
    ///     指定したモーションを開始する。同じタイプのモーションが既にある場合は、既存のモーションに終了フラグを立て、フェードアウトを開始させる。
    /// </summary>
    /// <param name="motion">開始するモーション</param>
    /// <param name="autoDelete">再生が終了したモーションのインスタンスを削除するなら true</param>
    /// <param name="userTimeSeconds">デルタ時間の積算値[秒]</param>
    /// <returns>開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」</returns>
    [Obsolete("Please use StartMotion(ACubismMotion motion")]
    public CubismMotionQueueEntry StartMotion(ACubismMotion motion, float userTimeSeconds)
    {
        CubismLog.Warning("StartMotion(ACubismMotion motion, float userTimeSeconds) is a deprecated function. Please use StartMotion(ACubismMotion motion).");

        CubismMotionQueueEntry motionQueueEntry;

        // 既にモーションがあれば終了フラグを立てる
        for ( var i = 0; i < Motions.Count; ++i )
        {
            motionQueueEntry = Motions[i];
            if ( motionQueueEntry == null )
            {
                continue;
            }

            motionQueueEntry.SetFadeout(motionQueueEntry.Motion.FadeOutSeconds);
        }

        motionQueueEntry = new CubismMotionQueueEntry { Motion = motion }; // 終了時に破棄する

        Motions.Add(motionQueueEntry);

        return motionQueueEntry;
    }

    /// <summary>
    ///     すべてのモーションが終了しているかどうか。
    /// </summary>
    /// <returns>
    ///     true    すべて終了している
    ///     false   終了していない
    /// </returns>
    public bool IsFinished()
    {
        // ------- 処理を行う --------
        // 既にモーションがあれば終了フラグを立てる
        foreach ( var item in Motions )
        {
            // ----- 終了済みの処理があれば削除する ------
            if ( !item.Finished )
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///     指定したモーションが終了しているかどうか。
    /// </summary>
    /// <param name="motionQueueEntryNumber">モーションの識別番号</param>
    /// <returns>
    ///     true    指定したモーションは終了している
    ///     false   終了していない
    /// </returns>
    public bool IsFinished(object motionQueueEntryNumber)
    {
        // 既にモーションがあれば終了フラグを立てる

        foreach ( var item in Motions )
        {
            if ( item == null )
            {
                continue;
            }

            if ( item == motionQueueEntryNumber && !item.Finished )
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///     すべてのモーションを停止する。
    /// </summary>
    public void StopAllMotions()
    {
        // ------- 処理を行う --------
        // 既にモーションがあれば終了フラグを立てる

        Motions.Clear();
    }

    /// <summary>
    ///     指定したCubismMotionQueueEntryを取得する。
    /// </summary>
    /// <param name="motionQueueEntryNumber">モーションの識別番号</param>
    /// <returns>
    ///     指定したCubismMotionQueueEntryへのポインタ
    ///     NULL   見つからなかった
    /// </returns>
    public CubismMotionQueueEntry? GetCubismMotionQueueEntry(object motionQueueEntryNumber)
    {
        //------- 処理を行う --------
        //既にモーションがあれば終了フラグを立てる

        foreach ( var item in Motions )
        {
            if ( item == motionQueueEntryNumber )
            {
                return item;
            }
        }

        return null;
    }

    /// <summary>
    ///     イベントを受け取るCallbackの登録をする。
    /// </summary>
    /// <param name="callback">コールバック関数</param>
    /// <param name="customData">コールバックに返されるデータ</param>
    public void SetEventCallback(CubismMotionEventFunction callback, CubismUserModel customData)
    {
        _eventCallback   = callback;
        _eventCustomData = customData;
    }

    /// <summary>
    ///     モーションを更新して、モデルにパラメータ値を反映する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="userTimeSeconds">デルタ時間の積算値[秒]</param>
    /// <returns>
    ///     true    モデルへパラメータ値の反映あり
    ///     false   モデルへパラメータ値の反映なし(モーションの変化なし)
    /// </returns>
    public virtual bool DoUpdateMotion(CubismModel model, float userTimeSeconds)
    {
        var updated = false;

        // ------- 処理を行う --------
        // 既にモーションがあれば終了フラグを立てる

        _remove.Clear();

        for ( var motionIndex = Motions.Count - 1; motionIndex >= 0; motionIndex-- )
        {
            var item   = Motions[motionIndex];
            var motion = item.Motion;

            // ------ 値を反映する ------
            motion.UpdateParameters(model, item, userTimeSeconds);
            updated = true;

            // ------ ユーザトリガーイベントを検査する ----
            var firedList = motion.GetFiredEvent(
                                                 item.LastEventCheckSeconds - item.StartTime,
                                                 userTimeSeconds - item.StartTime);

            for ( var i = 0; i < firedList.Count; ++i )
            {
                _eventCallback?.Invoke(_eventCustomData, firedList[i]);
            }

            item.LastEventCheckSeconds = userTimeSeconds;

            // ----- 終了済みの処理があれば削除する ------
            if ( item.Finished )
            {
                _remove.Add(item); // 削除
            }
            else
            {
                if ( item.IsTriggeredFadeOut )
                {
                    item.StartFadeout(item.FadeOutSeconds, userTimeSeconds);
                }
            }
        }

        foreach ( var item in _remove )
        {
            Motions.Remove(item);
        }

        return updated;
    }
}

using System.Globalization;

namespace PersonaEngine.Lib.TTS.Synthesis;

public static class Num2Words
{
    private static readonly string[] _units = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" };

    private static readonly string[] _tens = { "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" };

    private static readonly string[] _ordinalUnits = { "zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth" };

    private static readonly string[] _ordinalTens = { "", "", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetieth" };

    public static string Convert(int number, string to = "cardinal")
    {
        if ( number == 0 )
        {
            return _units[0];
        }

        if ( number < 0 )
        {
            return "negative " + Convert(-number, to);
        }

        return to.ToLower() switch {
            "ordinal" => ConvertToOrdinal(number),
            "year" => ConvertToYear(number),
            _ => ConvertToCardinal(number)
        };
    }

    public static string Convert(double number)
    {
        // Handle integer part
        var intPart = (int)Math.Floor(number);
        var result  = ConvertToCardinal(intPart);

        // Handle decimal part
        var decimalPart = number - intPart;
        if ( decimalPart > 0 )
        {
            // Get decimal digits as string and remove trailing zeros
            var decimalString = decimalPart.ToString("F20", CultureInfo.InvariantCulture)
                                           .TrimStart('0', '.')
                                           .TrimEnd('0');

            if ( !string.IsNullOrEmpty(decimalString) )
            {
                result += " point";
                foreach ( var digit in decimalString )
                {
                    result += " " + _units[int.Parse(digit.ToString())];
                }
            }
        }

        return result;
    }

    private static string ConvertToCardinal(int number)
    {
        if ( number < 20 )
        {
            return _units[number];
        }

        if ( number < 100 )
        {
            return _tens[number / 10] + (number % 10 > 0 ? "-" + _units[number % 10] : "");
        }

        if ( number < 1000 )
        {
            return _units[number / 100] + " hundred" + (number % 100 > 0 ? " and " + ConvertToCardinal(number % 100) : "");
        }

        if ( number < 1000000 )
        {
            var thousands = number / 1000;
            var remainder = number % 1000;

            var result = ConvertToCardinal(thousands) + " thousand";

            if ( remainder > 0 )
            {
                // If remainder is less than 100, or if it has a tens/units part
                if ( remainder < 100 )
                {
                    result += " and " + ConvertToCardinal(remainder);
                }
                else
                {
                    // For numbers like 1,234 -> "one thousand two hundred and thirty-four"
                    var hundreds     = remainder / 100;
                    var tensAndUnits = remainder % 100;

                    result += " " + _units[hundreds] + " hundred";
                    if ( tensAndUnits > 0 )
                    {
                        result += " and " + ConvertToCardinal(tensAndUnits);
                    }
                }
            }

            return result;
        }

        if ( number < 1000000000 )
        {
            return ConvertToCardinal(number / 1000000) + " million" + (number % 1000000 > 0 ? (number % 1000000 < 100 ? " and " : " ") + ConvertToCardinal(number % 1000000) : "");
        }

        return ConvertToCardinal(number / 1000000000) + " billion" + (number % 1000000000 > 0 ? (number % 1000000000 < 100 ? " and " : " ") + ConvertToCardinal(number % 1000000000) : "");
    }

    private static string ConvertToOrdinal(int number)
    {
        if ( number < 20 )
        {
            return _ordinalUnits[number];
        }

        if ( number < 100 )
        {
            if ( number % 10 == 0 )
            {
                return _ordinalTens[number / 10];
            }

            return _tens[number / 10] + "-" + _ordinalUnits[number % 10];
        }

        var cardinal = ConvertToCardinal(number);

        // Replace the last word with its ordinal form
        var lastSpace = cardinal.LastIndexOf(' ');
        if ( lastSpace == -1 )
        {
            // Single word
            var lastWord = cardinal;
            if ( lastWord.EndsWith("y") )
            {
                return lastWord.Substring(0, lastWord.Length - 1) + "ieth";
            }

            if ( lastWord.EndsWith("eight") )
            {
                return lastWord + "h";
            }

            if ( lastWord.EndsWith("nine") )
            {
                return lastWord + "th";
            }

            return lastWord + "th";
        }
        else
        {
            // Multiple words
            var lastWord = cardinal.Substring(lastSpace + 1);
            var prefix   = cardinal.Substring(0, lastSpace + 1);

            if ( lastWord.EndsWith("y") )
            {
                return prefix + lastWord.Substring(0, lastWord.Length - 1) + "ieth";
            }

            if ( lastWord == "one" )
            {
                return prefix + "first";
            }

            if ( lastWord == "two" )
            {
                return prefix + "second";
            }

            if ( lastWord == "three" )
            {
                return prefix + "third";
            }

            if ( lastWord == "five" )
            {
                return prefix + "fifth";
            }

            if ( lastWord == "eight" )
            {
                return prefix + "eighth";
            }

            if ( lastWord == "nine" || lastWord == "twelve" )
            {
                return prefix + lastWord + "th";
            }

            return prefix + lastWord + "th";
        }
    }

    private static string ConvertToYear(int year)
    {
        // Handle years specially
        if ( year >= 2000 )
        {
            // 2xxx is "two thousand [and] xxx"
            var remainder = year - 2000;
            if ( remainder == 0 )
            {
                return "two thousand";
            }

            if ( remainder < 100 )
            {
                return "two thousand and " + ConvertToCardinal(remainder);
            }

            return "two thousand " + ConvertToCardinal(remainder);
        }

        if ( year >= 1000 )
        {
            // Years like 1984 are "nineteen eighty-four"
            var century   = year / 100;
            var remainder = year % 100;

            if ( remainder == 0 )
            {
                return ConvertToCardinal(century) + " hundred";
            }

            return ConvertToCardinal(century) + " " + ConvertToCardinal(remainder);
        }

        // Years less than 1000
        return ConvertToCardinal(year);
    }
}

using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.Common;

internal struct UniformFieldInfo
{
    public int Location;

    public string Name;

    public int Size;

    public UniformType Type;
}

internal class Shader : IDisposable
{
    private readonly Dictionary<string, int> _attribLocation = new();

    private readonly (ShaderType Type, string Path)[] _files;

    private readonly GL _gl;

    private readonly Dictionary<string, int> _uniformToLocation = new();

    private bool _initialized = false;

    public Shader(GL gl, string vertexShader, string fragmentShader)
    {
        _gl     = gl;
        _files  = [(ShaderType.VertexShader, vertexShader), (ShaderType.FragmentShader, fragmentShader)];
        Program = CreateProgram(_files);
    }

    public uint Program { get; private set; }

    public void Dispose()
    {
        if ( _initialized )
        {
            _gl.DeleteProgram(Program);
            _initialized = false;
        }
    }

    public void Use()
    {
        _gl.UseProgram(Program);
        _gl.CheckError();
    }

    public void SetUniform(string name, int value)
    {
        var location = GetUniformLocation(name);
        if ( location == -1 )
        {
            throw new Exception($"{name} uniform not found on shader.");
        }

        _gl.Uniform1(location, value);
        _gl.CheckError();
    }

    public void SetUniform(string name, float value)
    {
        var location = GetUniformLocation(name);
        if ( location == -1 )
        {
            throw new Exception($"{name} uniform not found on shader.");
        }

        _gl.Uniform1(location, value);
        _gl.CheckError();
    }

    public void SetUniform(string name, float value, float value2)
    {
        var location = GetUniformLocation(name);
        if ( location == -1 )
        {
            throw new Exception($"{name} uniform not found on shader.");
        }

        _gl.Uniform2(location, value, value2);
        _gl.CheckError();
    }

    public unsafe void SetUniform(string name, Matrix4x4 value)
    {
        var location = GetUniformLocation(name);
        if ( location == -1 )
        {
            throw new Exception($"{name} uniform not found on shader.");
        }

        _gl.UniformMatrix4(location, 1, false, (float*)&value);
        _gl.CheckError();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int GetUniformLocation(string uniform)
    {
        if ( _uniformToLocation.TryGetValue(uniform, out var location) == false )
        {
            location = _gl.GetUniformLocation(Program, uniform);
            _uniformToLocation.Add(uniform, location);

            if ( location == -1 )
            {
                Debug.Print($"The uniform '{uniform}' does not exist in the shader!");
            }
        }

        return location;
    }

    public UniformFieldInfo[] GetUniforms()
    {
        _gl.GetProgram(Program, GLEnum.ActiveUniforms, out var uniformCount);

        var uniforms = new UniformFieldInfo[uniformCount];

        for ( var i = 0; i < uniformCount; i++ )
        {
            var name = _gl.GetActiveUniform(Program, (uint)i, out var size, out var type);

            UniformFieldInfo fieldInfo;
            fieldInfo.Location = GetUniformLocation(name);
            fieldInfo.Name     = name;
            fieldInfo.Size     = size;
            fieldInfo.Type     = type;

            uniforms[i] = fieldInfo;
        }

        return uniforms;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int GetAttribLocation(string attrib)
    {
        if ( _attribLocation.TryGetValue(attrib, out var location) == false )
        {
            location = _gl.GetAttribLocation(Program, attrib);
            _attribLocation.Add(attrib, location);

            if ( location == -1 )
            {
                Debug.Print($"The attrib '{attrib}' does not exist in the shader!");
            }
        }

        return location;
    }

    private uint CreateProgram(params (ShaderType Type, string source)[] shaderPaths)
    {
        var program = _gl.CreateProgram();

        Span<uint> shaders = stackalloc uint[shaderPaths.Length];
        for ( var i = 0; i < shaderPaths.Length; i++ )
        {
            shaders[i] = CompileShader(shaderPaths[i].Type, shaderPaths[i].source);
        }

        foreach ( var shader in shaders )
        {
            _gl.AttachShader(program, shader);
        }

        _gl.LinkProgram(program);

        _gl.GetProgram(program, GLEnum.LinkStatus, out var success);
        if ( success == 0 )
        {
            var info = _gl.GetProgramInfoLog(program);
            Debug.WriteLine($"GL.LinkProgram had info log:\n{info}");
        }

        foreach ( var shader in shaders )
        {
            _gl.DetachShader(program, shader);
            _gl.DeleteShader(shader);
        }

        _initialized = true;

        return program;
    }

    private uint CompileShader(ShaderType type, string source)
    {
        var shader = _gl.CreateShader(type);
        _gl.ShaderSource(shader, source);
        _gl.CompileShader(shader);

        _gl.GetShader(shader, ShaderParameterName.CompileStatus, out var success);
        if ( success == 0 )
        {
            var info = _gl.GetShaderInfoLog(shader);
            Debug.WriteLine($"GL.CompileShader for shader [{type}] had info log:\n{info}");
        }

        return shader;
    }
}

using System.Buffers.Binary;

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     This is a little helper for using multiple chunks of memory without writing it to auxiliary buffers
/// </summary>
/// <remarks>
///     It is used to consume the data that was appended in multiple calls and couldn't be used previously.
///     Note: If the requested chunks are not contiguous, the data will be copied to a new buffer.
/// </remarks>
public class MergedMemoryChunks
{
    private readonly List<ReadOnlyMemory<byte>> chunks = [];

    private int currentChunkIndex;

    public MergedMemoryChunks(ReadOnlyMemory<byte> initialChunk)
    {
        chunks.Add(initialChunk);
        Length = initialChunk.Length;
    }

    public MergedMemoryChunks() { }

    /// <summary>
    ///     The total length of the chunks
    /// </summary>
    public long Length { get; private set; }

    /// <summary>
    ///     The current position in the chunks
    /// </summary>
    public long Position { get; private set; }

    /// <summary>
    ///     The absolute position of the current chunk
    /// </summary>
    public long AbsolutePositionOfCurrentChunk { get; private set; }

    public void AddChunk(ReadOnlyMemory<byte> newChunk)
    {
        chunks.Add(newChunk);
        Length += newChunk.Length;
    }

    /// <summary>
    ///     Tries to skip the given number of bytes in the chunks, advancing the position.
    /// </summary>
    public bool TrySkip(uint count)
    {
        var bytesToSkip = count;
        while ( bytesToSkip > 0 )
        {
            var positionInCurrentChunk = Position - AbsolutePositionOfCurrentChunk;
            var currentChunk           = chunks[currentChunkIndex];
            if ( positionInCurrentChunk + bytesToSkip <= currentChunk.Length )
            {
                Position += bytesToSkip;

                return true;
            }

            if ( currentChunkIndex + 1 == chunks.Count )
            {
                return false;
            }

            var remainingInCurrentChunk = (int)(currentChunk.Length - positionInCurrentChunk);

            currentChunkIndex++;

            Position                       += remainingInCurrentChunk;
            AbsolutePositionOfCurrentChunk =  Position;
            bytesToSkip                    -= (uint)remainingInCurrentChunk;
        }

        return true;
    }

    /// <summary>
    ///     Restarts the reading from the beginning of the chunks
    /// </summary>
    public void RestartRead()
    {
        currentChunkIndex              = 0;
        AbsolutePositionOfCurrentChunk = 0;
        Position                       = 0;
    }

    /// <summary>
    ///     Gets a slice of the given size from the chunks
    /// </summary>
    /// <remarks>
    ///     If the data will span multiple chunks, it will be copied to a new buffer.
    /// </remarks>
    public ReadOnlyMemory<byte> GetChunk(int size)
    {
        var positionInCurrentChunk = (int)(Position - AbsolutePositionOfCurrentChunk);
        var currentChunk           = chunks[currentChunkIndex];
        // First, we try to just slice the current chunk if possible
        if ( currentChunk.Length >= positionInCurrentChunk + size )
        {
            Position += size;
            if ( currentChunk.Length == positionInCurrentChunk + size )
            {
                currentChunkIndex++;
                AbsolutePositionOfCurrentChunk = Position;
            }

            return currentChunk.Slice(positionInCurrentChunk, size);
        }

        // We cannot slice it, so we need to compose it
        var buffer        = new byte[size];
        var bufferIndex   = 0;
        var remainingSize = size;
        while ( remainingSize > 0 )
        {
            var currentChunkAddressable = currentChunk.Slice(positionInCurrentChunk, Math.Min(remainingSize, currentChunk.Length - positionInCurrentChunk));

            remainingSize -= currentChunkAddressable.Length;
            Position      += currentChunkAddressable.Length;
            currentChunkAddressable.CopyTo(buffer.AsMemory(bufferIndex));
            bufferIndex += currentChunkAddressable.Length;
            if ( remainingSize > 0 && currentChunkIndex >= chunks.Count )
            {
                throw new InvalidOperationException($"Not enough data was available in the chunks to read {size} bytes.");
            }

            if ( remainingSize > 0 )
            {
                positionInCurrentChunk = 0;
                currentChunkIndex++;
                AbsolutePositionOfCurrentChunk = Position;
                currentChunk                   = chunks[currentChunkIndex];
            }
        }

        return buffer.AsMemory();
    }

    /// <summary>
    ///     Reads a 32-bit unsigned integer from the chunks
    /// </summary>
    public uint ReadUInt32LittleEndian()
    {
        var chunk = GetChunk(4);

        return BinaryPrimitives.ReadUInt32LittleEndian(chunk.Span);
    }

    /// <summary>
    ///     Reads a 16-bit unsigned integer from the chunks
    /// </summary>
    public ushort ReadUInt16LittleEndian()
    {
        var chunk = GetChunk(2);

        return BinaryPrimitives.ReadUInt16LittleEndian(chunk.Span);
    }

    /// <summary>
    ///     Reads a 8-bit unsigned integer from the chunks
    /// </summary>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    public byte ReadByte()
    {
        var chunk = GetChunk(1);

        return chunk.Span[0];
    }

    public short ReadInt16LittleEndian()
    {
        var chunk = GetChunk(2);

        return BinaryPrimitives.ReadInt16LittleEndian(chunk.Span);
    }

    public int ReadInt24LittleEndian()
    {
        var chunk = GetChunk(3);

        return chunk.Span[0] | (chunk.Span[1] << 8) | (chunk.Span[2] << 16);
    }

    public int ReadInt32LittleEndian()
    {
        var chunk = GetChunk(4);

        return BinaryPrimitives.ReadInt32LittleEndian(chunk.Span);
    }

    public long ReadInt64LittleEndian()
    {
        var chunk = GetChunk(8);

        return BinaryPrimitives.ReadInt64LittleEndian(chunk.Span);
    }

    /// <summary>
    ///     Copies the content of the current chunks to a single buffer
    /// </summary>
    /// <returns></returns>
    public byte[] ToArray()
    {
        var buffer      = new byte[Length];
        var bufferIndex = 0;
        foreach ( var chunk in chunks )
        {
            chunk.Span.CopyTo(buffer.AsSpan(bufferIndex));
            bufferIndex += chunk.Length;
        }

        return buffer;
    }
}

using PersonaEngine.Lib.Live2D.Framework.Effect;
using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Motion;
using PersonaEngine.Lib.Live2D.Framework.Physics;
using PersonaEngine.Lib.Live2D.Framework.Rendering;

namespace PersonaEngine.Lib.Live2D.Framework.Model;

/// <summary>
///     ユーザーが実際に使用するモデルの基底クラス。これを継承してユーザーが実装する。
/// </summary>
public abstract class CubismUserModel : IDisposable
{
    /// <summary>
    ///     X軸方向の加速度
    /// </summary>
    protected float _accelerationX;

    /// <summary>
    ///     Y軸方向の加速度
    /// </summary>
    protected float _accelerationY;

    /// <summary>
    ///     Z軸方向の加速度
    /// </summary>
    protected float _accelerationZ;

    /// <summary>
    ///     呼吸
    /// </summary>
    protected CubismBreath _breath;

    /// <summary>
    ///     マウスドラッグ
    /// </summary>
    protected CubismTargetPoint _dragManager;

    /// <summary>
    ///     マウスドラッグのX位置
    /// </summary>
    protected float _dragX;

    /// <summary>
    ///     マウスドラッグのY位置
    /// </summary>
    protected float _dragY;

    /// <summary>
    ///     表情管理
    /// </summary>
    protected CubismExpressionMotionManager _expressionManager;

    /// <summary>
    ///     自動まばたき
    /// </summary>
    protected CubismEyeBlink? _eyeBlink;

    /// <summary>
    ///     最後のリップシンクの制御値
    /// </summary>
    protected float _lastLipSyncValue;

    /// <summary>
    ///     リップシンクするかどうか
    /// </summary>
    protected bool _lipSync;

    /// <summary>
    ///     Mocデータ
    /// </summary>
    protected CubismMoc _moc;

    /// <summary>
    ///     MOC3整合性検証するかどうか
    /// </summary>
    protected bool _mocConsistency;

    /// <summary>
    ///     ユーザデータ
    /// </summary>
    protected CubismModelUserData? _modelUserData;

    /// <summary>
    ///     モーション管理
    /// </summary>
    protected CubismMotionManager _motionManager;

    /// <summary>
    ///     物理演算
    /// </summary>
    protected CubismPhysics? _physics;

    /// <summary>
    ///     ポーズ管理
    /// </summary>
    protected CubismPose? _pose;

    /// <summary>
    ///     コンストラクタ。
    /// </summary>
    public CubismUserModel()
    {
        _lipSync = true;

        Opacity = 1.0f;

        // モーションマネージャーを作成
        // MotionQueueManagerクラスからの継承なので使い方は同じ
        _motionManager = new CubismMotionManager();
        _motionManager.SetEventCallback(CubismDefaultMotionEventCallback, this);

        // 表情モーションマネージャを作成
        _expressionManager = new CubismExpressionMotionManager();

        // ドラッグによるアニメーション
        _dragManager = new CubismTargetPoint();
    }

    /// <summary>
    ///     レンダラ
    /// </summary>
    public CubismRenderer? Renderer { get; private set; }

    /// <summary>
    ///     モデル行列
    /// </summary>
    public CubismModelMatrix ModelMatrix { get; protected set; }

    /// <summary>
    ///     Modelインスタンス
    /// </summary>
    public CubismModel Model => _moc.Model;

    /// <summary>
    ///     初期化されたかどうか
    /// </summary>
    public bool Initialized { get; set; }

    /// <summary>
    ///     更新されたかどうか
    /// </summary>
    public bool Updating { get; set; }

    /// <summary>
    ///     不透明度
    /// </summary>
    public float Opacity { get; set; }

    public void Dispose()
    {
        _moc.Dispose();

        DeleteRenderer();
    }

    /// <summary>
    ///     CubismMotionQueueManagerにイベント用に登録するためのCallback。
    ///     CubismUserModelの継承先のEventFiredを呼ぶ。
    /// </summary>
    /// <param name="eventValue">発火したイベントの文字列データ</param>
    /// <param name="customData">CubismUserModelを継承したインスタンスを想定</param>
    public static void CubismDefaultMotionEventCallback(CubismUserModel? customData, string eventValue) { customData?.MotionEventFired(eventValue); }

    /// <summary>
    ///     マウスドラッグの情報を設定する。
    /// </summary>
    /// <param name="x">ドラッグしているカーソルのX位置</param>
    /// <param name="y">ドラッグしているカーソルのY位置</param>
    public void SetDragging(float x, float y) { _dragManager.Set(x, y); }

    /// <summary>
    ///     加速度の情報を設定する。
    /// </summary>
    /// <param name="x">X軸方向の加速度</param>
    /// <param name="y">Y軸方向の加速度</param>
    /// <param name="z">Z軸方向の加速度</param>
    protected void SetAcceleration(float x, float y, float z)
    {
        _accelerationX = x;
        _accelerationY = y;
        _accelerationZ = z;
    }

    /// <summary>
    ///     モデルデータを読み込む。
    /// </summary>
    /// <param name="buffer">moc3ファイルが読み込まれているバッファ</param>
    /// <param name="shouldCheckMocConsistency">MOCの整合性チェックフラグ(初期値 : false)</param>
    protected void LoadModel(byte[] buffer, bool shouldCheckMocConsistency = false)
    {
        _moc = new CubismMoc(buffer, shouldCheckMocConsistency);
        Model.SaveParameters();
        ModelMatrix = new CubismModelMatrix(Model.GetCanvasWidth(), Model.GetCanvasHeight());
    }

    /// <summary>
    ///     ポーズデータを読み込む。
    /// </summary>
    /// <param name="buffer">pose3.jsonが読み込まれているバッファ</param>
    protected void LoadPose(string buffer) { _pose = new CubismPose(buffer); }

    /// <summary>
    ///     物理演算データを読み込む。
    /// </summary>
    /// <param name="buffer">physics3.jsonが読み込まれているバッファ</param>
    protected void LoadPhysics(string buffer) { _physics = new CubismPhysics(buffer); }

    /// <summary>
    ///     ユーザーデータを読み込む。
    /// </summary>
    /// <param name="buffer">userdata3.jsonが読み込まれているバッファ</param>
    protected void LoadUserData(string buffer) { _modelUserData = new CubismModelUserData(buffer); }

    /// <summary>
    ///     指定した位置にDrawableがヒットしているかどうかを取得する。
    /// </summary>
    /// <param name="drawableId">検証したいDrawableのID</param>
    /// <param name="pointX">X位置</param>
    /// <param name="pointY">Y位置</param>
    /// <returns>
    ///     true    ヒットしている
    ///     false   ヒットしていない
    /// </returns>
    public unsafe bool IsHit(string drawableId, float pointX, float pointY)
    {
        var drawIndex = Model.GetDrawableIndex(drawableId);

        if ( drawIndex < 0 )
        {
            return false; // 存在しない場合はfalse
        }

        var count    = Model.GetDrawableVertexCount(drawIndex);
        var vertices = Model.GetDrawableVertices(drawIndex);

        var left   = vertices[0];
        var right  = vertices[0];
        var top    = vertices[1];
        var bottom = vertices[1];

        for ( var j = 1; j < count; ++j )
        {
            var x = vertices[CubismFramework.VertexOffset + j * CubismFramework.VertexStep];
            var y = vertices[CubismFramework.VertexOffset + j * CubismFramework.VertexStep + 1];

            if ( x < left )
            {
                left = x; // Min x
            }

            if ( x > right )
            {
                right = x; // Max x
            }

            if ( y < top )
            {
                top = y; // Min y
            }

            if ( y > bottom )
            {
                bottom = y; // Max y
            }
        }

        var tx = ModelMatrix.InvertTransformX(pointX);
        var ty = ModelMatrix.InvertTransformY(pointY);

        return left <= tx && tx <= right && top <= ty && ty <= bottom;
    }

    /// <summary>
    ///     レンダラを生成して初期化を実行する。
    /// </summary>
    protected void CreateRenderer(CubismRenderer renderer)
    {
        if ( Renderer != null )
        {
            DeleteRenderer();
        }

        Renderer = renderer;
    }

    /// <summary>
    ///     レンダラを解放する。
    /// </summary>
    protected void DeleteRenderer()
    {
        if ( Renderer != null )
        {
            Renderer.Dispose();
            Renderer = null;
        }
    }

    /// <summary>
    ///     Eventが再生処理時にあった場合の処理をする。
    ///     継承で上書きすることを想定している。
    ///     上書きしない場合はログ出力をする。
    /// </summary>
    /// <param name="eventValue">発火したイベントの文字列データ</param>
    protected abstract void MotionEventFired(string eventValue);
}

using Microsoft.Extensions.Logging;

using OpenNLP.Tools.SentenceDetect;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Implementation of OpenNLP-based sentence detection
/// </summary>
public class OpenNlpSentenceDetector : IMlSentenceDetector
{
    private readonly EnglishMaximumEntropySentenceDetector _detector;

    private readonly ILogger<OpenNlpSentenceDetector> _logger;

    private bool _disposed;

    public OpenNlpSentenceDetector(string modelPath, ILogger<OpenNlpSentenceDetector> logger)
    {
        if ( string.IsNullOrEmpty(modelPath) )
        {
            throw new ArgumentException("Model path cannot be null or empty", nameof(modelPath));
        }

        _logger = logger ?? throw new ArgumentNullException(nameof(logger));

        try
        {
            _detector = new EnglishMaximumEntropySentenceDetector(modelPath);
            _logger.LogInformation("Initialized OpenNLP sentence detector from {ModelPath}", modelPath);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize OpenNLP sentence detector");

            throw;
        }
    }

    /// <summary>
    ///     Detects sentences in text using OpenNLP
    /// </summary>
    public IReadOnlyList<string> Detect(string text)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return Array.Empty<string>();
        }

        try
        {
            var sentences = _detector.SentenceDetect(text);

            return sentences;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error detecting sentences");

            throw;
        }
    }

    public void Dispose()
    {
        if ( _disposed )
        {
            return;
        }

        _disposed = true;
    }
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

public class CubismClippingManager_OpenGLES2(OpenGLApi gl) : CubismClippingManager(RenderType.OpenGL)
{
    /// <summary>
    ///     クリッピングコンテキストを作成する。モデル描画時に実行する。
    /// </summary>
    /// <param name="model">モデルのインスタンス</param>
    /// <param name="renderer">レンダラのインスタンス</param>
    /// <param name="lastFBO">フレームバッファ</param>
    /// <param name="lastViewport">ビューポート</param>
    internal unsafe void SetupClippingContext(CubismModel model, CubismRenderer_OpenGLES2 renderer, int lastFBO, int[] lastViewport)
    {
        // 全てのクリッピングを用意する
        // 同じクリップ（複数の場合はまとめて１つのクリップ）を使う場合は１度だけ設定する
        var usingClipCount = 0;
        for ( var clipIndex = 0; clipIndex < ClippingContextListForMask.Count; clipIndex++ )
        {
            // １つのクリッピングマスクに関して
            var cc = ClippingContextListForMask[clipIndex];

            // このクリップを利用する描画オブジェクト群全体を囲む矩形を計算
            CalcClippedDrawTotalBounds(model, cc!);

            if ( cc!.IsUsing )
            {
                usingClipCount++; //使用中としてカウント
            }
        }

        if ( usingClipCount <= 0 )
        {
            return;
        }

        // マスク作成処理
        // 生成したOffscreenSurfaceと同じサイズでビューポートを設定
        gl.Viewport(0, 0, (int)ClippingMaskBufferSize.X, (int)ClippingMaskBufferSize.Y);

        // 後の計算のためにインデックスの最初をセット
        CurrentMaskBuffer = renderer.GetMaskBuffer(0);
        // ----- マスク描画処理 -----
        CurrentMaskBuffer.BeginDraw(lastFBO);

        renderer.PreDraw(); // バッファをクリアする

        // 各マスクのレイアウトを決定していく
        SetupLayoutBounds(usingClipCount);

        // サイズがレンダーテクスチャの枚数と合わない場合は合わせる
        if ( ClearedMaskBufferFlags.Count != RenderTextureCount )
        {
            ClearedMaskBufferFlags.Clear();

            for ( var i = 0; i < RenderTextureCount; ++i )
            {
                ClearedMaskBufferFlags.Add(false);
            }
        }
        else
        {
            // マスクのクリアフラグを毎フレーム開始時に初期化
            for ( var i = 0; i < RenderTextureCount; ++i )
            {
                ClearedMaskBufferFlags[i] = false;
            }
        }

        // 実際にマスクを生成する
        // 全てのマスクをどの様にレイアウトして描くかを決定し、ClipContext , ClippedDrawContext に記憶する
        for ( var clipIndex = 0; clipIndex < ClippingContextListForMask.Count; clipIndex++ )
        {
            // --- 実際に１つのマスクを描く ---
            var clipContext         = ClippingContextListForMask[clipIndex];
            var allClippedDrawRect  = clipContext!.AllClippedDrawRect; //このマスクを使う、全ての描画オブジェクトの論理座標上の囲み矩形
            var layoutBoundsOnTex01 = clipContext.LayoutBounds;        //この中にマスクを収める
            var MARGIN              = 0.05f;

            // clipContextに設定したオフスクリーンサーフェイスをインデックスで取得
            var clipContextOffscreenSurface = renderer.GetMaskBuffer(clipContext.BufferIndex);

            // 現在のオフスクリーンサーフェイスがclipContextのものと異なる場合
            if ( CurrentMaskBuffer != clipContextOffscreenSurface )
            {
                CurrentMaskBuffer.EndDraw();
                CurrentMaskBuffer = clipContextOffscreenSurface;
                // マスク用RenderTextureをactiveにセット
                CurrentMaskBuffer.BeginDraw(lastFBO);

                // バッファをクリアする。
                renderer.PreDraw();
            }

            // モデル座標上の矩形を、適宜マージンを付けて使う
            TmpBoundsOnModel.SetRect(allClippedDrawRect);
            TmpBoundsOnModel.Expand(allClippedDrawRect.Width * MARGIN, allClippedDrawRect.Height * MARGIN);
            //########## 本来は割り当てられた領域の全体を使わず必要最低限のサイズがよい
            // シェーダ用の計算式を求める。回転を考慮しない場合は以下のとおり
            // movePeriod' = movePeriod * scaleX + offX     [[ movePeriod' = (movePeriod - tmpBoundsOnModel.movePeriod)*scale + layoutBoundsOnTex01.movePeriod ]]
            var scaleX = layoutBoundsOnTex01.Width / TmpBoundsOnModel.Width;
            var scaleY = layoutBoundsOnTex01.Height / TmpBoundsOnModel.Height;

            // マスク生成時に使う行列を求める
            CreateMatrixForMask(false, layoutBoundsOnTex01, scaleX, scaleY);

            clipContext.MatrixForMask.SetMatrix(TmpMatrixForMask.Tr);
            clipContext.MatrixForDraw.SetMatrix(TmpMatrixForDraw.Tr);

            // 実際の描画を行う
            var clipDrawCount = clipContext.ClippingIdCount;
            for ( var i = 0; i < clipDrawCount; i++ )
            {
                var clipDrawIndex = clipContext.ClippingIdList[i];

                // 頂点情報が更新されておらず、信頼性がない場合は描画をパスする
                if ( !model.GetDrawableDynamicFlagVertexPositionsDidChange(clipDrawIndex) )
                {
                    continue;
                }

                renderer.IsCulling = model.GetDrawableCulling(clipDrawIndex);

                // マスクがクリアされていないなら処理する
                if ( !ClearedMaskBufferFlags[clipContext.BufferIndex] )
                {
                    // マスクをクリアする
                    // 1が無効（描かれない）領域、0が有効（描かれる）領域。（シェーダーCd*Csで0に近い値をかけてマスクを作る。1をかけると何も起こらない）
                    gl.ClearColor(1.0f, 1.0f, 1.0f, 1.0f);
                    gl.Clear(gl.GL_COLOR_BUFFER_BIT);
                    ClearedMaskBufferFlags[clipContext.BufferIndex] = true;
                }

                // 今回専用の変換を適用して描く
                // チャンネルも切り替える必要がある(A,R,G,B)
                renderer.ClippingContextBufferForMask = clipContext;

                renderer.DrawMeshOpenGL(model, clipDrawIndex);
            }
        }

        // --- 後処理 ---
        CurrentMaskBuffer.EndDraw();
        renderer.ClippingContextBufferForMask = null;
        gl.Viewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3]);
    }
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

internal class CubismShader_OpenGLES2(OpenGLApi gl)
{
    public const string CSM_FRAGMENT_SHADER_FP_PRECISION_HIGH = "highp";

    public const string CSM_FRAGMENT_SHADER_FP_PRECISION_MID = "mediump";

    public const string CSM_FRAGMENT_SHADER_FP_PRECISION_LOW = "lowp";

    public const string CSM_FRAGMENT_SHADER_FP_PRECISION = CSM_FRAGMENT_SHADER_FP_PRECISION_HIGH;

    private const string GLES2 = "#version 100\n";

    private const string GLES2C = GLES2 + "precision " + CSM_FRAGMENT_SHADER_FP_PRECISION + " float;";

    private const string Normal = "#version 120\n";

    private const string Tegra = "#version 100\n" +
                                 "#extension GL_NV_shader_framebuffer_fetch : enable\n" +
                                 "precision " + CSM_FRAGMENT_SHADER_FP_PRECISION + " float;";

    // SetupMask
    public const string VertShaderSrcSetupMask_ES2 =
        GLES2 + VertShaderSrcSetupMask_Base;

    public const string VertShaderSrcSetupMask_Normal =
        Normal + VertShaderSrcSetupMask_Base;

    private const string VertShaderSrcSetupMask_Base =
        @"attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
varying vec4 v_myPos;
uniform mat4 u_clipMatrix;
void main()
{
gl_Position = u_clipMatrix * a_position;
v_myPos = u_clipMatrix * a_position;
v_texCoord = a_texCoord;
v_texCoord.y = 1.0 - v_texCoord.y;
}";

    public const string FragShaderSrcSetupMask_ES2 = GLES2C + FragShaderSrcSetupMask_Base;

    public const string FragShaderSrcSetupMask_Normal = Normal + FragShaderSrcSetupMask_Base;

    private const string FragShaderSrcSetupMask_Base =
        @"varying vec2 v_texCoord;
varying vec4 v_myPos;
uniform sampler2D s_texture0;
uniform vec4 u_channelFlag;
uniform vec4 u_baseColor;
void main()
{
float isInside =
    step(u_baseColor.x, v_myPos.x/v_myPos.w)
* step(u_baseColor.y, v_myPos.y/v_myPos.w)
* step(v_myPos.x/v_myPos.w, u_baseColor.z)
* step(v_myPos.y/v_myPos.w, u_baseColor.w);
gl_FragColor = u_channelFlag * texture2D(s_texture0 , v_texCoord).a * isInside;
}";

    public const string FragShaderSrcSetupMaskTegra =
        Tegra + FragShaderSrcSetupMask_Base;

    //----- バーテックスシェーダプログラム -----
    // Normal & Add & Mult 共通
    public const string VertShaderSrc_ES2 = GLES2 + VertShaderSrc_Base;

    public const string VertShaderSrc_Normal = Normal + VertShaderSrc_Base;

    private const string VertShaderSrc_Base =
        @"attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
uniform mat4 u_matrix;
void main()
{
gl_Position = u_matrix * a_position;
v_texCoord = a_texCoord;
v_texCoord.y = 1.0 - v_texCoord.y;
}";

    // Normal & Add & Mult 共通（クリッピングされたものの描画用）
    public const string VertShaderSrcMasked_ES2 = GLES2 + VertShaderSrcMasked_Base;

    public const string VertShaderSrcMasked_Normal = Normal + VertShaderSrcMasked_Base;

    private const string VertShaderSrcMasked_Base =
        @"attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
varying vec4 v_clipPos;
uniform mat4 u_matrix;
uniform mat4 u_clipMatrix;
void main()
{
gl_Position = u_matrix * a_position;
v_clipPos = u_clipMatrix * a_position;
v_texCoord = a_texCoord;
v_texCoord.y = 1.0 - v_texCoord.y;
}";

    //----- フラグメントシェーダプログラム -----
    // Normal & Add & Mult 共通
    public const string FragShaderSrc_ES2 = GLES2C + FragShaderSrc_Base;

    public const string FragShaderSrc_Normal = Normal + FragShaderSrc_Base;

    public const string FragShaderSrc_Base =
        @"varying vec2 v_texCoord;
uniform sampler2D s_texture0;
uniform vec4 u_baseColor;
uniform vec4 u_multiplyColor;
uniform vec4 u_screenColor;
void main()
{
vec4 texColor = texture2D(s_texture0 , v_texCoord);
texColor.rgb = texColor.rgb * u_multiplyColor.rgb;
texColor.rgb = texColor.rgb + u_screenColor.rgb - (texColor.rgb * u_screenColor.rgb);
vec4 color = texColor * u_baseColor;
gl_FragColor = vec4(color.rgb * color.a,  color.a);
}";

    public const string FragShaderSrcTegra = Tegra + FragShaderSrc_Base;

    // Normal & Add & Mult 共通 （PremultipliedAlpha）
    public const string FragShaderSrcPremultipliedAlpha_ES2 = GLES2C + FragShaderSrcPremultipliedAlpha_Base;

    public const string FragShaderSrcPremultipliedAlpha_Normal = Normal + FragShaderSrcPremultipliedAlpha_Base;

    public const string FragShaderSrcPremultipliedAlpha_Base =
        @"varying vec2 v_texCoord;
uniform sampler2D s_texture0;
uniform vec4 u_baseColor;
uniform vec4 u_multiplyColor;
uniform vec4 u_screenColor;
void main()
{
vec4 texColor = texture2D(s_texture0 , v_texCoord);
texColor.rgb = texColor.rgb * u_multiplyColor.rgb;
texColor.rgb = (texColor.rgb + u_screenColor.rgb * texColor.a) - (texColor.rgb * u_screenColor.rgb);
gl_FragColor = texColor * u_baseColor;
}";

    public const string FragShaderSrcPremultipliedAlphaTegra = Tegra + FragShaderSrcPremultipliedAlpha_Base;

    // Normal & Add & Mult 共通（クリッピングされたものの描画用）
    public const string FragShaderSrcMask_ES2 = GLES2C + FragShaderSrcMask_Base;

    public const string FragShaderSrcMask_Normal = Normal + FragShaderSrcMask_Base;

    public const string FragShaderSrcMask_Base =
        @"varying vec2 v_texCoord;
varying vec4 v_clipPos;
uniform sampler2D s_texture0;
uniform sampler2D s_texture1;
uniform vec4 u_channelFlag;
uniform vec4 u_baseColor;
uniform vec4 u_multiplyColor;
uniform vec4 u_screenColor;
void main()
{
vec4 texColor = texture2D(s_texture0 , v_texCoord);
texColor.rgb = texColor.rgb * u_multiplyColor.rgb;
texColor.rgb = texColor.rgb + u_screenColor.rgb - (texColor.rgb * u_screenColor.rgb);
vec4 col_formask = texColor * u_baseColor;
col_formask.rgb = col_formask.rgb  * col_formask.a ;
vec4 clipMask = (1.0 - texture2D(s_texture1, v_clipPos.xy / v_clipPos.w)) * u_channelFlag;
float maskVal = clipMask.r + clipMask.g + clipMask.b + clipMask.a;
col_formask = col_formask * maskVal;
gl_FragColor = col_formask;
}";

    public const string FragShaderSrcMaskTegra = Tegra + FragShaderSrcMask_Base;

    // Normal & Add & Mult 共通（クリッピングされて反転使用の描画用）
    public const string FragShaderSrcMaskInverted_ES2 = GLES2C + FragShaderSrcMaskInverted_Base;

    public const string FragShaderSrcMaskInverted_Normal = Normal + FragShaderSrcMaskInverted_Base;

    public const string FragShaderSrcMaskInverted_Base =
        @"varying vec2 v_texCoord;
varying vec4 v_clipPos;
uniform sampler2D s_texture0;
uniform sampler2D s_texture1;
uniform vec4 u_channelFlag;
uniform vec4 u_baseColor;
uniform vec4 u_multiplyColor;
uniform vec4 u_screenColor;
void main()
{
vec4 texColor = texture2D(s_texture0 , v_texCoord);
texColor.rgb = texColor.rgb * u_multiplyColor.rgb;
texColor.rgb = texColor.rgb + u_screenColor.rgb - (texColor.rgb * u_screenColor.rgb);
vec4 col_formask = texColor * u_baseColor;
col_formask.rgb = col_formask.rgb  * col_formask.a ;
vec4 clipMask = (1.0 - texture2D(s_texture1, v_clipPos.xy / v_clipPos.w)) * u_channelFlag;
float maskVal = clipMask.r + clipMask.g + clipMask.b + clipMask.a;
col_formask = col_formask * (1.0 - maskVal);
gl_FragColor = col_formask;
}";

    public const string FragShaderSrcMaskInvertedTegra = Tegra + FragShaderSrcMaskInverted_Base;

    // Normal & Add & Mult 共通（クリッピングされたものの描画用、PremultipliedAlphaの場合）
    public const string FragShaderSrcMaskPremultipliedAlpha_ES2 = GLES2C + FragShaderSrcMaskPremultipliedAlpha_Base;

    public const string FragShaderSrcMaskPremultipliedAlpha_Normal = Normal + FragShaderSrcMaskPremultipliedAlpha_Base;

    public const string FragShaderSrcMaskPremultipliedAlpha_Base =
        @"varying vec2 v_texCoord;
        varying vec4 v_clipPos;
        uniform sampler2D s_texture0;
        uniform sampler2D s_texture1;
        uniform vec4 u_channelFlag;
        uniform vec4 u_baseColor;
        uniform vec4 u_multiplyColor;
        uniform vec4 u_screenColor;
        void main()
        {
        vec4 texColor = texture2D(s_texture0 , v_texCoord);
        texColor.rgb = texColor.rgb * u_multiplyColor.rgb;
        texColor.rgb = (texColor.rgb + u_screenColor.rgb * texColor.a) - (texColor.rgb * u_screenColor.rgb);
        vec4 col_formask = texColor * u_baseColor;
        vec4 clipMask = (1.0 - texture2D(s_texture1, v_clipPos.xy / v_clipPos.w)) * u_channelFlag;
        float maskVal = clipMask.r + clipMask.g + clipMask.b + clipMask.a;
        col_formask = col_formask * maskVal;
        gl_FragColor = col_formask;
        }";

    public const string FragShaderSrcMaskPremultipliedAlphaTegra = Tegra + FragShaderSrcMaskPremultipliedAlpha_Base;

    // Normal & Add & Mult 共通（クリッピングされて反転使用の描画用、PremultipliedAlphaの場合）
    public const string FragShaderSrcMaskInvertedPremultipliedAlpha_ES2 = GLES2C + FragShaderSrcMaskInvertedPremultipliedAlpha_Base;

    public const string FragShaderSrcMaskInvertedPremultipliedAlpha_Normal = Normal + FragShaderSrcMaskInvertedPremultipliedAlpha_Base;

    public const string FragShaderSrcMaskInvertedPremultipliedAlpha_Base =
        @"varying vec2 v_texCoord;
varying vec4 v_clipPos;
uniform sampler2D s_texture0;
uniform sampler2D s_texture1;
uniform vec4 u_channelFlag;
uniform vec4 u_baseColor;
uniform vec4 u_multiplyColor;
uniform vec4 u_screenColor;
void main()
{
vec4 texColor = texture2D(s_texture0 , v_texCoord);
texColor.rgb = texColor.rgb * u_multiplyColor.rgb;
texColor.rgb = (texColor.rgb + u_screenColor.rgb * texColor.a) - (texColor.rgb * u_screenColor.rgb);
vec4 col_formask = texColor * u_baseColor;
vec4 clipMask = (1.0 - texture2D(s_texture1, v_clipPos.xy / v_clipPos.w)) * u_channelFlag;
float maskVal = clipMask.r + clipMask.g + clipMask.b + clipMask.a;
col_formask = col_formask * (1.0 - maskVal);
gl_FragColor = col_formask;
}";

    public const string FragShaderSrcMaskInvertedPremultipliedAlphaTegra = Tegra + FragShaderSrcMaskInvertedPremultipliedAlpha_Base;

    public const int ShaderCount = 19; // シェーダの数 = マスク生成用 + (通常 + 加算 + 乗算) * (マスク無 + マスク有 + マスク有反転 + マスク無の乗算済アルファ対応版 + マスク有の乗算済アルファ対応版 + マスク有反転の乗算済アルファ対応版)

    /// <summary>
    ///     ロードしたシェーダプログラムを保持する変数
    /// </summary>
    private readonly List<CubismShaderSet> _shaderSets = [];

    /// <summary>
    ///     Tegra対応.拡張方式で描画
    /// </summary>
    internal bool s_extMode;

    /// <summary>
    ///     拡張方式のPA設定用の変数
    /// </summary>
    internal bool s_extPAMode;

    /// <summary>
    ///     シェーダプログラムの一連のセットアップを実行する
    /// </summary>
    /// <param name="renderer">レンダラのインスタンス</param>
    /// <param name="textureId">GPUのテクスチャID</param>
    /// <param name="vertexCount">ポリゴンメッシュの頂点数</param>
    /// <param name="vertexArray">ポリゴンメッシュの頂点配列</param>
    /// <param name="uvArray">uv配列</param>
    /// <param name="opacity">不透明度</param>
    /// <param name="colorBlendMode">カラーブレンディングのタイプ</param>
    /// <param name="baseColor">ベースカラー</param>
    /// <param name="multiplyColor"></param>
    /// <param name="screenColor"></param>
    /// <param name="isPremultipliedAlpha">乗算済みアルファかどうか</param>
    /// <param name="matrix4x4">Model-View-Projection行列</param>
    /// <param name="invertedMask">マスクを反転して使用するフラグ</param>
    internal void SetupShaderProgramForDraw(CubismRenderer_OpenGLES2 renderer, CubismModel model, int index)
    {
        if ( _shaderSets.Count == 0 )
        {
            GenerateShaders();
        }

        // Blending
        int SRC_COLOR;
        int DST_COLOR;
        int SRC_ALPHA;
        int DST_ALPHA;

        // _shaderSets用のオフセット計算
        var masked               = renderer.ClippingContextBufferForDraw != null; // この描画オブジェクトはマスク対象か
        var invertedMask         = model.GetDrawableInvertedMask(index);
        var isPremultipliedAlpha = renderer.IsPremultipliedAlpha;
        var offset               = (masked ? invertedMask ? 2 : 1 : 0) + (isPremultipliedAlpha ? 3 : 0);

        CubismShaderSet shaderSet;
        switch ( model.GetDrawableBlendMode(index) )
        {
            case CubismBlendMode.Normal:
            default:
                shaderSet = _shaderSets[(int)ShaderNames.Normal + offset];
                SRC_COLOR = gl.GL_ONE;
                DST_COLOR = gl.GL_ONE_MINUS_SRC_ALPHA;
                SRC_ALPHA = gl.GL_ONE;
                DST_ALPHA = gl.GL_ONE_MINUS_SRC_ALPHA;

                break;

            case CubismBlendMode.Additive:
                shaderSet = _shaderSets[(int)ShaderNames.Add + offset];
                SRC_COLOR = gl.GL_ONE;
                DST_COLOR = gl.GL_ONE;
                SRC_ALPHA = gl.GL_ZERO;
                DST_ALPHA = gl.GL_ONE;

                break;

            case CubismBlendMode.Multiplicative:
                shaderSet = _shaderSets[(int)ShaderNames.Mult + offset];
                SRC_COLOR = gl.GL_DST_COLOR;
                DST_COLOR = gl.GL_ONE_MINUS_SRC_ALPHA;
                SRC_ALPHA = gl.GL_ZERO;
                DST_ALPHA = gl.GL_ONE;

                break;
        }

        gl.UseProgram(shaderSet.ShaderProgram);

        // 頂点配列の設定
        SetupTexture(renderer, model, index, shaderSet);

        // テクスチャ頂点の設定
        SetVertexAttributes(shaderSet);

        if ( masked )
        {
            gl.ActiveTexture(gl.GL_TEXTURE1);

            var draw = renderer.ClippingContextBufferForDraw!;

            // frameBufferに書かれたテクスチャ
            var tex = renderer.GetMaskBuffer(draw.BufferIndex).ColorBuffer;

            gl.BindTexture(gl.GL_TEXTURE_2D, tex);
            gl.Uniform1i(shaderSet.SamplerTexture1Location, 1);

            // View座標をClippingContextの座標に変換するための行列を設定
            gl.UniformMatrix4fv(shaderSet.UniformClipMatrixLocation, 1, false, draw.MatrixForDraw.Tr);

            // 使用するカラーチャンネルを設定
            SetColorChannelUniformVariables(shaderSet, renderer.ClippingContextBufferForDraw!);
        }

        //座標変換
        gl.UniformMatrix4fv(shaderSet.UniformMatrixLocation, 1, false, renderer.GetMvpMatrix().Tr);

        // ユニフォーム変数設定
        var baseColor     = renderer.GetModelColorWithOpacity(model.GetDrawableOpacity(index));
        var multiplyColor = model.GetMultiplyColor(index);
        var screenColor   = model.GetScreenColor(index);
        SetColorUniformVariables(shaderSet, baseColor, multiplyColor, screenColor);

        gl.BlendFuncSeparate(SRC_COLOR, DST_COLOR, SRC_ALPHA, DST_ALPHA);
    }

    internal void SetupShaderProgramForMask(CubismRenderer_OpenGLES2 renderer, CubismModel model, int index)
    {
        if ( _shaderSets.Count == 0 )
        {
            GenerateShaders();
        }

        // Blending
        var SRC_COLOR = gl.GL_ZERO;
        var DST_COLOR = gl.GL_ONE_MINUS_SRC_COLOR;
        var SRC_ALPHA = gl.GL_ZERO;
        var DST_ALPHA = gl.GL_ONE_MINUS_SRC_ALPHA;

        var shaderSet = _shaderSets[(int)ShaderNames.SetupMask];
        gl.UseProgram(shaderSet.ShaderProgram);

        var draw = renderer.ClippingContextBufferForMask!;

        //テクスチャ設定
        SetupTexture(renderer, model, index, shaderSet);

        // 頂点配列の設定
        SetVertexAttributes(shaderSet);

        // 使用するカラーチャンネルを設定
        SetColorChannelUniformVariables(shaderSet, draw);

        gl.UniformMatrix4fv(shaderSet.UniformClipMatrixLocation, 1, false, draw.MatrixForMask.Tr);

        var                rect          = draw.LayoutBounds;
        CubismTextureColor baseColor     = new(rect.X * 2.0f - 1.0f, rect.Y * 2.0f - 1.0f, rect.GetRight() * 2.0f - 1.0f, rect.GetBottom() * 2.0f - 1.0f);
        var                multiplyColor = model.GetMultiplyColor(index);
        var                screenColor   = model.GetScreenColor(index);
        SetColorUniformVariables(shaderSet, baseColor, multiplyColor, screenColor);

        gl.BlendFuncSeparate(SRC_COLOR, DST_COLOR, SRC_ALPHA, DST_ALPHA);
    }

    /// <summary>
    ///     シェーダプログラムを解放する
    /// </summary>
    internal void ReleaseShaderProgram()
    {
        for ( var i = 0; i < _shaderSets.Count; i++ )
        {
            if ( _shaderSets[i].ShaderProgram != 0 )
            {
                gl.DeleteProgram(_shaderSets[i].ShaderProgram);
                _shaderSets[i].ShaderProgram = 0;
            }
        }
    }

    /// <summary>
    ///     シェーダプログラムを初期化する
    /// </summary>
    internal void GenerateShaders()
    {
        for ( var i = 0; i < ShaderCount; i++ )
        {
            _shaderSets.Add(new CubismShaderSet());
        }

        if ( gl.IsES2 )
        {
            if ( s_extMode )
            {
                _shaderSets[0].ShaderProgram = LoadShaderProgram(VertShaderSrcSetupMask_ES2, FragShaderSrcSetupMaskTegra);

                _shaderSets[1].ShaderProgram = LoadShaderProgram(VertShaderSrc_ES2, FragShaderSrcTegra);
                _shaderSets[2].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMaskTegra);
                _shaderSets[3].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMaskInvertedTegra);
                _shaderSets[4].ShaderProgram = LoadShaderProgram(VertShaderSrc_ES2, FragShaderSrcPremultipliedAlphaTegra);
                _shaderSets[5].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMaskPremultipliedAlphaTegra);
                _shaderSets[6].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMaskInvertedPremultipliedAlphaTegra);
            }
            else
            {
                _shaderSets[0].ShaderProgram = LoadShaderProgram(VertShaderSrcSetupMask_ES2, FragShaderSrcSetupMask_ES2);

                _shaderSets[1].ShaderProgram = LoadShaderProgram(VertShaderSrc_ES2, FragShaderSrc_ES2);
                _shaderSets[2].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMask_ES2);
                _shaderSets[3].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMaskInverted_ES2);
                _shaderSets[4].ShaderProgram = LoadShaderProgram(VertShaderSrc_ES2, FragShaderSrcPremultipliedAlpha_ES2);
                _shaderSets[5].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMaskPremultipliedAlpha_ES2);
                _shaderSets[6].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_ES2, FragShaderSrcMaskInvertedPremultipliedAlpha_ES2);
            }

            // 加算も通常と同じシェーダーを利用する
            _shaderSets[7].ShaderProgram  = _shaderSets[1].ShaderProgram;
            _shaderSets[8].ShaderProgram  = _shaderSets[2].ShaderProgram;
            _shaderSets[9].ShaderProgram  = _shaderSets[3].ShaderProgram;
            _shaderSets[10].ShaderProgram = _shaderSets[4].ShaderProgram;
            _shaderSets[11].ShaderProgram = _shaderSets[5].ShaderProgram;
            _shaderSets[12].ShaderProgram = _shaderSets[6].ShaderProgram;

            // 乗算も通常と同じシェーダーを利用する
            _shaderSets[13].ShaderProgram = _shaderSets[1].ShaderProgram;
            _shaderSets[14].ShaderProgram = _shaderSets[2].ShaderProgram;
            _shaderSets[15].ShaderProgram = _shaderSets[3].ShaderProgram;
            _shaderSets[16].ShaderProgram = _shaderSets[4].ShaderProgram;
            _shaderSets[17].ShaderProgram = _shaderSets[5].ShaderProgram;
            _shaderSets[18].ShaderProgram = _shaderSets[6].ShaderProgram;
        }
        else
        {
            _shaderSets[0].ShaderProgram = LoadShaderProgram(VertShaderSrcSetupMask_Normal, FragShaderSrcSetupMask_Normal);

            _shaderSets[1].ShaderProgram = LoadShaderProgram(VertShaderSrc_Normal, FragShaderSrc_Normal);
            _shaderSets[2].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_Normal, FragShaderSrcMask_Normal);
            _shaderSets[3].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_Normal, FragShaderSrcMaskInverted_Normal);
            _shaderSets[4].ShaderProgram = LoadShaderProgram(VertShaderSrc_Normal, FragShaderSrcPremultipliedAlpha_Normal);
            _shaderSets[5].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_Normal, FragShaderSrcMaskPremultipliedAlpha_Normal);
            _shaderSets[6].ShaderProgram = LoadShaderProgram(VertShaderSrcMasked_Normal, FragShaderSrcMaskInvertedPremultipliedAlpha_Normal);

            // 加算も通常と同じシェーダーを利用する
            _shaderSets[7].ShaderProgram  = _shaderSets[1].ShaderProgram;
            _shaderSets[8].ShaderProgram  = _shaderSets[2].ShaderProgram;
            _shaderSets[9].ShaderProgram  = _shaderSets[3].ShaderProgram;
            _shaderSets[10].ShaderProgram = _shaderSets[4].ShaderProgram;
            _shaderSets[11].ShaderProgram = _shaderSets[5].ShaderProgram;
            _shaderSets[12].ShaderProgram = _shaderSets[6].ShaderProgram;

            // 乗算も通常と同じシェーダーを利用する
            _shaderSets[13].ShaderProgram = _shaderSets[1].ShaderProgram;
            _shaderSets[14].ShaderProgram = _shaderSets[2].ShaderProgram;
            _shaderSets[15].ShaderProgram = _shaderSets[3].ShaderProgram;
            _shaderSets[16].ShaderProgram = _shaderSets[4].ShaderProgram;
            _shaderSets[17].ShaderProgram = _shaderSets[5].ShaderProgram;
            _shaderSets[18].ShaderProgram = _shaderSets[6].ShaderProgram;
        }

        // SetupMask
        _shaderSets[0].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[0].ShaderProgram, "a_position");
        _shaderSets[0].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[0].ShaderProgram, "a_texCoord");
        _shaderSets[0].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[0].ShaderProgram, "s_texture0");
        _shaderSets[0].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[0].ShaderProgram, "u_clipMatrix");
        _shaderSets[0].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[0].ShaderProgram, "u_channelFlag");
        _shaderSets[0].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[0].ShaderProgram, "u_baseColor");
        _shaderSets[0].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[0].ShaderProgram, "u_multiplyColor");
        _shaderSets[0].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[0].ShaderProgram, "u_screenColor");

        // 通常
        _shaderSets[1].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[1].ShaderProgram, "a_position");
        _shaderSets[1].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[1].ShaderProgram, "a_texCoord");
        _shaderSets[1].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[1].ShaderProgram, "s_texture0");
        _shaderSets[1].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[1].ShaderProgram, "u_matrix");
        _shaderSets[1].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[1].ShaderProgram, "u_baseColor");
        _shaderSets[1].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[1].ShaderProgram, "u_multiplyColor");
        _shaderSets[1].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[1].ShaderProgram, "u_screenColor");

        // 通常（クリッピング）
        _shaderSets[2].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[2].ShaderProgram, "a_position");
        _shaderSets[2].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[2].ShaderProgram, "a_texCoord");
        _shaderSets[2].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "s_texture0");
        _shaderSets[2].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "s_texture1");
        _shaderSets[2].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "u_matrix");
        _shaderSets[2].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "u_clipMatrix");
        _shaderSets[2].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "u_channelFlag");
        _shaderSets[2].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "u_baseColor");
        _shaderSets[2].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "u_multiplyColor");
        _shaderSets[2].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[2].ShaderProgram, "u_screenColor");

        // 通常（クリッピング・反転）
        _shaderSets[3].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[3].ShaderProgram, "a_position");
        _shaderSets[3].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[3].ShaderProgram, "a_texCoord");
        _shaderSets[3].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "s_texture0");
        _shaderSets[3].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "s_texture1");
        _shaderSets[3].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "u_matrix");
        _shaderSets[3].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "u_clipMatrix");
        _shaderSets[3].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "u_channelFlag");
        _shaderSets[3].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "u_baseColor");
        _shaderSets[3].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "u_multiplyColor");
        _shaderSets[3].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[3].ShaderProgram, "u_screenColor");

        // 通常（PremultipliedAlpha）
        _shaderSets[4].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[4].ShaderProgram, "a_position");
        _shaderSets[4].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[4].ShaderProgram, "a_texCoord");
        _shaderSets[4].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[4].ShaderProgram, "s_texture0");
        _shaderSets[4].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[4].ShaderProgram, "u_matrix");
        _shaderSets[4].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[4].ShaderProgram, "u_baseColor");
        _shaderSets[4].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[4].ShaderProgram, "u_multiplyColor");
        _shaderSets[4].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[4].ShaderProgram, "u_screenColor");

        // 通常（クリッピング、PremultipliedAlpha）
        _shaderSets[5].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[5].ShaderProgram, "a_position");
        _shaderSets[5].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[5].ShaderProgram, "a_texCoord");
        _shaderSets[5].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "s_texture0");
        _shaderSets[5].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "s_texture1");
        _shaderSets[5].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "u_matrix");
        _shaderSets[5].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "u_clipMatrix");
        _shaderSets[5].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "u_channelFlag");
        _shaderSets[5].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "u_baseColor");
        _shaderSets[5].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "u_multiplyColor");
        _shaderSets[5].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[5].ShaderProgram, "u_screenColor");

        // 通常（クリッピング・反転、PremultipliedAlpha）
        _shaderSets[6].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[6].ShaderProgram, "a_position");
        _shaderSets[6].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[6].ShaderProgram, "a_texCoord");
        _shaderSets[6].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "s_texture0");
        _shaderSets[6].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "s_texture1");
        _shaderSets[6].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "u_matrix");
        _shaderSets[6].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "u_clipMatrix");
        _shaderSets[6].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "u_channelFlag");
        _shaderSets[6].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "u_baseColor");
        _shaderSets[6].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "u_multiplyColor");
        _shaderSets[6].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[6].ShaderProgram, "u_screenColor");

        // 加算
        _shaderSets[7].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[7].ShaderProgram, "a_position");
        _shaderSets[7].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[7].ShaderProgram, "a_texCoord");
        _shaderSets[7].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[7].ShaderProgram, "s_texture0");
        _shaderSets[7].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[7].ShaderProgram, "u_matrix");
        _shaderSets[7].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[7].ShaderProgram, "u_baseColor");
        _shaderSets[7].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[7].ShaderProgram, "u_multiplyColor");
        _shaderSets[7].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[7].ShaderProgram, "u_screenColor");

        // 加算（クリッピング）
        _shaderSets[8].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[8].ShaderProgram, "a_position");
        _shaderSets[8].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[8].ShaderProgram, "a_texCoord");
        _shaderSets[8].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "s_texture0");
        _shaderSets[8].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "s_texture1");
        _shaderSets[8].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "u_matrix");
        _shaderSets[8].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "u_clipMatrix");
        _shaderSets[8].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "u_channelFlag");
        _shaderSets[8].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "u_baseColor");
        _shaderSets[8].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "u_multiplyColor");
        _shaderSets[8].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[8].ShaderProgram, "u_screenColor");

        // 加算（クリッピング・反転）
        _shaderSets[9].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[9].ShaderProgram, "a_position");
        _shaderSets[9].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[9].ShaderProgram, "a_texCoord");
        _shaderSets[9].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "s_texture0");
        _shaderSets[9].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "s_texture1");
        _shaderSets[9].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "u_matrix");
        _shaderSets[9].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "u_clipMatrix");
        _shaderSets[9].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "u_channelFlag");
        _shaderSets[9].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "u_baseColor");
        _shaderSets[9].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "u_multiplyColor");
        _shaderSets[9].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[9].ShaderProgram, "u_screenColor");

        // 加算（PremultipliedAlpha）
        _shaderSets[10].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[10].ShaderProgram, "a_position");
        _shaderSets[10].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[10].ShaderProgram, "a_texCoord");
        _shaderSets[10].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[10].ShaderProgram, "s_texture0");
        _shaderSets[10].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[10].ShaderProgram, "u_matrix");
        _shaderSets[10].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[10].ShaderProgram, "u_baseColor");
        _shaderSets[10].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[10].ShaderProgram, "u_multiplyColor");
        _shaderSets[10].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[10].ShaderProgram, "u_screenColor");

        // 加算（クリッピング、PremultipliedAlpha）
        _shaderSets[11].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[11].ShaderProgram, "a_position");
        _shaderSets[11].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[11].ShaderProgram, "a_texCoord");
        _shaderSets[11].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "s_texture0");
        _shaderSets[11].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "s_texture1");
        _shaderSets[11].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "u_matrix");
        _shaderSets[11].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "u_clipMatrix");
        _shaderSets[11].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "u_channelFlag");
        _shaderSets[11].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "u_baseColor");
        _shaderSets[11].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "u_multiplyColor");
        _shaderSets[11].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[11].ShaderProgram, "u_screenColor");

        // 加算（クリッピング・反転、PremultipliedAlpha）
        _shaderSets[12].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[12].ShaderProgram, "a_position");
        _shaderSets[12].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[12].ShaderProgram, "a_texCoord");
        _shaderSets[12].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "s_texture0");
        _shaderSets[12].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "s_texture1");
        _shaderSets[12].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "u_matrix");
        _shaderSets[12].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "u_clipMatrix");
        _shaderSets[12].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "u_channelFlag");
        _shaderSets[12].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "u_baseColor");
        _shaderSets[12].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "u_multiplyColor");
        _shaderSets[12].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[12].ShaderProgram, "u_screenColor");

        // 乗算
        _shaderSets[13].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[13].ShaderProgram, "a_position");
        _shaderSets[13].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[13].ShaderProgram, "a_texCoord");
        _shaderSets[13].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[13].ShaderProgram, "s_texture0");
        _shaderSets[13].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[13].ShaderProgram, "u_matrix");
        _shaderSets[13].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[13].ShaderProgram, "u_baseColor");
        _shaderSets[13].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[13].ShaderProgram, "u_multiplyColor");
        _shaderSets[13].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[13].ShaderProgram, "u_screenColor");

        // 乗算（クリッピング）
        _shaderSets[14].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[14].ShaderProgram, "a_position");
        _shaderSets[14].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[14].ShaderProgram, "a_texCoord");
        _shaderSets[14].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "s_texture0");
        _shaderSets[14].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "s_texture1");
        _shaderSets[14].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "u_matrix");
        _shaderSets[14].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "u_clipMatrix");
        _shaderSets[14].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "u_channelFlag");
        _shaderSets[14].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "u_baseColor");
        _shaderSets[14].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "u_multiplyColor");
        _shaderSets[14].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[14].ShaderProgram, "u_screenColor");

        // 乗算（クリッピング・反転）
        _shaderSets[15].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[15].ShaderProgram, "a_position");
        _shaderSets[15].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[15].ShaderProgram, "a_texCoord");
        _shaderSets[15].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "s_texture0");
        _shaderSets[15].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "s_texture1");
        _shaderSets[15].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "u_matrix");
        _shaderSets[15].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "u_clipMatrix");
        _shaderSets[15].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "u_channelFlag");
        _shaderSets[15].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "u_baseColor");
        _shaderSets[15].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "u_multiplyColor");
        _shaderSets[15].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[15].ShaderProgram, "u_screenColor");

        // 乗算（PremultipliedAlpha）
        _shaderSets[16].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[16].ShaderProgram, "a_position");
        _shaderSets[16].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[16].ShaderProgram, "a_texCoord");
        _shaderSets[16].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[16].ShaderProgram, "s_texture0");
        _shaderSets[16].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[16].ShaderProgram, "u_matrix");
        _shaderSets[16].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[16].ShaderProgram, "u_baseColor");
        _shaderSets[16].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[16].ShaderProgram, "u_multiplyColor");
        _shaderSets[16].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[16].ShaderProgram, "u_screenColor");

        // 乗算（クリッピング、PremultipliedAlpha）
        _shaderSets[17].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[17].ShaderProgram, "a_position");
        _shaderSets[17].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[17].ShaderProgram, "a_texCoord");
        _shaderSets[17].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "s_texture0");
        _shaderSets[17].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "s_texture1");
        _shaderSets[17].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "u_matrix");
        _shaderSets[17].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "u_clipMatrix");
        _shaderSets[17].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "u_channelFlag");
        _shaderSets[17].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "u_baseColor");
        _shaderSets[17].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "u_multiplyColor");
        _shaderSets[17].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[17].ShaderProgram, "u_screenColor");

        // 乗算（クリッピング・反転、PremultipliedAlpha）
        _shaderSets[18].AttributePositionLocation    = gl.GetAttribLocation(_shaderSets[18].ShaderProgram, "a_position");
        _shaderSets[18].AttributeTexCoordLocation    = gl.GetAttribLocation(_shaderSets[18].ShaderProgram, "a_texCoord");
        _shaderSets[18].SamplerTexture0Location      = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "s_texture0");
        _shaderSets[18].SamplerTexture1Location      = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "s_texture1");
        _shaderSets[18].UniformMatrixLocation        = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "u_matrix");
        _shaderSets[18].UniformClipMatrixLocation    = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "u_clipMatrix");
        _shaderSets[18].UnifromChannelFlagLocation   = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "u_channelFlag");
        _shaderSets[18].UniformBaseColorLocation     = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "u_baseColor");
        _shaderSets[18].UniformMultiplyColorLocation = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "u_multiplyColor");
        _shaderSets[18].UniformScreenColorLocation   = gl.GetUniformLocation(_shaderSets[18].ShaderProgram, "u_screenColor");
    }

    /// <summary>
    ///     シェーダプログラムをロードしてアドレス返す。
    /// </summary>
    /// <param name="vertShaderSrc">頂点シェーダのソース</param>
    /// <param name="fragShaderSrc">フラグメントシェーダのソース</param>
    /// <returns>シェーダプログラムのアドレス</returns>
    internal int LoadShaderProgram(string vertShaderSrc, string fragShaderSrc)
    {
        // Create shader program.
        var shaderProgram = gl.CreateProgram();

        if ( !CompileShaderSource(out var vertShader, gl.GL_VERTEX_SHADER, vertShaderSrc) )
        {
            CubismLog.Error("[Live2D SDK]Vertex shader compile error!");

            return 0;
        }

        // Create and compile fragment shader.
        if ( !CompileShaderSource(out var fragShader, gl.GL_FRAGMENT_SHADER, fragShaderSrc) )
        {
            CubismLog.Error("[Live2D SDK]Fragment shader compile error!");

            return 0;
        }

        // Attach vertex shader to program.
        gl.AttachShader(shaderProgram, vertShader);

        // Attach fragment shader to program.
        gl.AttachShader(shaderProgram, fragShader);

        // Link program.
        if ( !LinkProgram(shaderProgram) )
        {
            CubismLog.Error("[Live2D SDK]Failed to link program: %d", shaderProgram);

            if ( vertShader != 0 )
            {
                gl.DeleteShader(vertShader);
            }

            if ( fragShader != 0 )
            {
                gl.DeleteShader(fragShader);
            }

            if ( shaderProgram != 0 )
            {
                gl.DeleteProgram(shaderProgram);
            }

            return 0;
        }

        // Release vertex and fragment shaders.
        if ( vertShader != 0 )
        {
            gl.DetachShader(shaderProgram, vertShader);
            gl.DeleteShader(vertShader);
        }

        if ( fragShader != 0 )
        {
            gl.DetachShader(shaderProgram, fragShader);
            gl.DeleteShader(fragShader);
        }

        return shaderProgram;
    }

    /// <summary>
    ///     シェーダプログラムをコンパイルする
    /// </summary>
    /// <param name="outShader">コンパイルされたシェーダプログラムのアドレス</param>
    /// <param name="shaderType">シェーダタイプ(Vertex/Fragment)</param>
    /// <param name="shaderSource">シェーダソースコード</param>
    /// <returns>
    ///     true         .      コンパイル成功
    ///     false        .      コンパイル失敗
    /// </returns>
    internal unsafe bool CompileShaderSource(out int outShader, int shaderType, string shaderSource)
    {
        int status;

        outShader = gl.CreateShader(shaderType);
        gl.ShaderSource(outShader, shaderSource);
        gl.CompileShader(outShader);

        int logLength;
        gl.GetShaderiv(outShader, gl.GL_INFO_LOG_LENGTH, &logLength);
        if ( logLength > 0 )
        {
            gl.GetShaderInfoLog(outShader, out var log);
            CubismLog.Error($"[Live2D SDK]Shader compile log: {log}");
        }

        gl.GetShaderiv(outShader, gl.GL_COMPILE_STATUS, &status);
        if ( status == gl.GL_FALSE )
        {
            gl.DeleteShader(outShader);

            return false;
        }

        return true;
    }

    /// <summary>
    ///     シェーダプログラムをリンクする
    /// </summary>
    /// <param name="shaderProgram">リンクするシェーダプログラムのアドレス</param>
    /// <returns>
    ///     true            .  リンク成功
    ///     false           .  リンク失敗
    /// </returns>
    internal unsafe bool LinkProgram(int shaderProgram)
    {
        int status;
        gl.LinkProgram(shaderProgram);

        int logLength;
        gl.GetProgramiv(shaderProgram, gl.GL_INFO_LOG_LENGTH, &logLength);
        if ( logLength > 0 )
        {
            gl.GetProgramInfoLog(shaderProgram, out var log);
            CubismLog.Error($"[Live2D SDK]Program link log: {log}");
        }

        gl.GetProgramiv(shaderProgram, gl.GL_LINK_STATUS, &status);
        if ( status == gl.GL_FALSE )
        {
            return false;
        }

        return true;
    }

    /// <summary>
    ///     シェーダプログラムを検証する
    /// </summary>
    /// <param name="shaderProgram">検証するシェーダプログラムのアドレス</param>
    /// <returns>
    ///     true            .  正常
    ///     false           .  異常
    /// </returns>
    internal unsafe bool ValidateProgram(int shaderProgram)
    {
        int logLength,
            status;

        gl.ValidateProgram(shaderProgram);
        gl.GetProgramiv(shaderProgram, gl.GL_INFO_LOG_LENGTH, &logLength);
        if ( logLength > 0 )
        {
            gl.GetProgramInfoLog(shaderProgram, out var log);
            CubismLog.Error($"[Live2D SDK]Validate program log: {log}");
        }

        gl.GetProgramiv(shaderProgram, gl.GL_VALIDATE_STATUS, &status);
        if ( status == gl.GL_FALSE )
        {
            return false;
        }

        return true;
    }

    /// <summary>
    ///     Tegraプロセッサ対応。拡張方式による描画の有効・無効
    /// </summary>
    /// <param name="extMode">trueなら拡張方式で描画する</param>
    /// <param name="extPAMode">trueなら拡張方式のPA設定を有効にする</param>
    internal void SetExtShaderMode(bool extMode, bool extPAMode)
    {
        s_extMode   = extMode;
        s_extPAMode = extPAMode;
    }

    /// <summary>
    ///     必要な頂点属性を設定する
    /// </summary>
    /// <param name="model">描画対象のモデル</param>
    /// <param name="index">描画対象のメッシュのインデックス</param>
    /// <param name="shaderSet">シェーダープログラムのセット</param>
    public void SetVertexAttributes(CubismShaderSet shaderSet)
    {
        // 頂点位置属性の設定
        gl.EnableVertexAttribArray(shaderSet.AttributePositionLocation);
        gl.VertexAttribPointer(shaderSet.AttributePositionLocation, 2, gl.GL_FLOAT, false, 4 * sizeof(float), 0);

        // テクスチャ座標属性の設定
        gl.EnableVertexAttribArray(shaderSet.AttributeTexCoordLocation);
        gl.VertexAttribPointer(shaderSet.AttributeTexCoordLocation, 2, gl.GL_FLOAT, false, 4 * sizeof(float), 2 * sizeof(float));
    }

    /// <summary>
    ///     テクスチャの設定を行う
    /// </summary>
    /// <param name="renderer">レンダラー</param>
    /// <param name="model">描画対象のモデル</param>
    /// <param name="index">描画対象のメッシュのインデックス</param>
    /// <param name="shaderSet">シェーダープログラムのセット</param>
    public void SetupTexture(CubismRenderer_OpenGLES2 renderer, CubismModel model, int index, CubismShaderSet shaderSet)
    {
        var textureIndex = model.GetDrawableTextureIndex(index);
        var textureId    = renderer.GetBindedTextureId(textureIndex);
        gl.ActiveTexture(gl.GL_TEXTURE0);
        gl.BindTexture(gl.GL_TEXTURE_2D, textureId);
        gl.Uniform1i(shaderSet.SamplerTexture0Location, 0);
    }

    /// <summary>
    ///     色関連のユニフォーム変数の設定を行う
    /// </summary>
    /// <param name="renderer">レンダラー</param>
    /// <param name="model">描画対象のモデル</param>
    /// <param name="index">描画対象のメッシュのインデックス</param>
    /// <param name="shaderSet">シェーダープログラムのセット</param>
    /// <param name="baseColor">ベースカラー</param>
    /// <param name="multiplyColor">乗算カラー</param>
    /// <param name="screenColor">スクリーンカラー</param>
    public void SetColorUniformVariables(CubismShaderSet    shaderSet,
                                         CubismTextureColor baseColor, CubismTextureColor multiplyColor, CubismTextureColor screenColor)
    {
        gl.Uniform4f(shaderSet.UniformBaseColorLocation, baseColor.R, baseColor.G, baseColor.B, baseColor.A);
        gl.Uniform4f(shaderSet.UniformMultiplyColorLocation, multiplyColor.R, multiplyColor.G, multiplyColor.B, multiplyColor.A);
        gl.Uniform4f(shaderSet.UniformScreenColorLocation, screenColor.R, screenColor.G, screenColor.B, screenColor.A);
    }

    /// <summary>
    ///     カラーチャンネル関連のユニフォーム変数の設定を行う
    /// </summary>
    /// <param name="shaderSet">シェーダープログラムのセット</param>
    /// <param name="contextBuffer">描画コンテクスト</param>
    public void SetColorChannelUniformVariables(CubismShaderSet shaderSet, CubismClippingContext contextBuffer)
    {
        var channelIndex = contextBuffer.LayoutChannelIndex;
        var colorChannel = contextBuffer.Manager.GetChannelFlagAsColor(channelIndex);
        gl.Uniform4f(shaderSet.UnifromChannelFlagLocation, colorChannel.R, colorChannel.G, colorChannel.B, colorChannel.A);
    }
}

namespace PersonaEngine.Lib.UI.RouletteWheel;

public partial class RouletteWheel
{
    /// <summary>
    ///     Defines the animation state for the wheel visibility transitions.
    /// </summary>
    public enum AnimationState
    {
        /// <summary>
        ///     Wheel is idle and not animating.
        /// </summary>
        Idle,

        /// <summary>
        ///     Wheel is animating to an enabled state.
        /// </summary>
        AnimatingIn,

        /// <summary>
        ///     Wheel is animating to a disabled state.
        /// </summary>
        AnimatingOut
    }

    private float _animationCurrentScale = 1.0f;

    private float _animationDuration = 0.3f; // Animation duration in seconds

    private float _animationStartScale = 1.0f;

    private float _animationStartTime = 0f;

    private float _animationTargetScale = 1.0f;

    /// <summary>
    ///     Gets whether the wheel is currently enabled.
    /// </summary>
    public bool IsEnabled { get; private set; } = true;

    /// <summary>
    ///     Gets the current animation state of the wheel.
    /// </summary>
    public AnimationState CurrentAnimationState { get; private set; } = AnimationState.Idle;

    /// <summary>
    ///     Gets whether the wheel is currently animating.
    /// </summary>
    public bool IsAnimating => CurrentAnimationState != AnimationState.Idle;

    /// <summary>
    ///     Enables the wheel with an optional animation.
    /// </summary>
    public void Enable()
    {
        if ( IsEnabled && CurrentAnimationState == AnimationState.Idle )
        {
            return;
        }

        IsEnabled = true;

        if ( !_config.CurrentValue.AnimateToggle )
        {
            CurrentAnimationState  = AnimationState.Idle;
            _animationCurrentScale = 1.0f;

            return;
        }

        CurrentAnimationState = AnimationState.AnimatingIn;
        _animationStartTime   = _time;
        _animationDuration    = _config.CurrentValue.AnimationDuration;
        _animationStartScale  = _animationCurrentScale;
        _animationTargetScale = 1.0f;
    }

    /// <summary>
    ///     Disables the wheel with an optional animation.
    /// </summary>
    public void Disable()
    {
        if ( !IsEnabled && CurrentAnimationState == AnimationState.Idle )
        {
            return;
        }

        IsEnabled = false;

        if ( !_config.CurrentValue.AnimateToggle )
        {
            CurrentAnimationState  = AnimationState.Idle;
            _animationCurrentScale = 0.0f;

            return;
        }

        CurrentAnimationState = AnimationState.AnimatingOut;
        _animationStartTime   = _time;
        _animationDuration    = _config.CurrentValue.AnimationDuration;
        _animationStartScale  = _animationCurrentScale;
        _animationTargetScale = 0.0f;
    }

    /// <summary>
    ///     Toggles the wheel's enabled state with an optional animation.
    /// </summary>
    /// <param name="animate">Whether to animate the transition.</param>
    /// <param name="duration">Duration of the animation in seconds.</param>
    public void Toggle()
    {
        if ( IsEnabled )
        {
            Disable();
        }
        else
        {
            Enable();
        }
    }

    private void UpdateVisibilityAnimation()
    {
        if ( CurrentAnimationState == AnimationState.Idle )
        {
            return;
        }

        var elapsed  = _time - _animationStartTime;
        var progress = Math.Min(elapsed / _animationDuration, 1.0f);

        // Apply easing function to the progress
        var easedProgress = EaseOutBack(progress);

        // Calculate current scale based on progress
        _animationCurrentScale = _animationStartScale + (_animationTargetScale - _animationStartScale) * easedProgress;

        // Check if animation is complete
        if ( progress >= 1.0f )
        {
            CurrentAnimationState  = AnimationState.Idle;
            _animationCurrentScale = _animationTargetScale;
        }
    }

    private static float EaseOutBack(float t)
    {
        const float c1 = 1.70158f;
        const float c3 = c1 + 1;

        return 1 + c3 * MathF.Pow(t - 1, 3) + c1 * MathF.Pow(t - 1, 2);
    }
}

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;
using PersonaEngine.Lib.Core.Conversation.Implementations.Context;
using PersonaEngine.Lib.Core.Conversation.Implementations.Metrics;
using PersonaEngine.Lib.Core.Conversation.Implementations.Strategies;
using PersonaEngine.Lib.LLM;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Session;

public class ConversationSessionFactory(
    ILoggerFactory             loggerFactory,
    IChatEngine                chatEngine,
    ITtsEngine                 ttsEngine,
    IEnumerable<IInputAdapter> inputAdapters,
    IOutputAdapter             outputAdapter,
    ConversationMetrics        metrics)
    : IConversationSessionFactory
{
    public IConversationSession CreateSession(ConversationContext context, ConversationOptions? options = null, Guid? sessionId = null)
    {
        var logger = loggerFactory.CreateLogger<ConversationSession>();
        options ??= new ConversationOptions();

        return new ConversationSession(
                                       logger,
                                       chatEngine,
                                       ttsEngine,
                                       inputAdapters,
                                       outputAdapter,
                                       metrics,
                                       sessionId ?? Guid.NewGuid(),
                                       options,
                                       context,
                                       GetBargeInStrategy(options!)
                                      );
    }

    private IBargeInStrategy GetBargeInStrategy(ConversationOptions options)
    {
        return options.BargeInType switch {
            BargeInType.Ignore => new IgnoreBargeInStrategy(),
            BargeInType.Allow => new AllowBargeInStrategy(),
            BargeInType.NoSpeaking => new NoSpeakingBargeInStrategy(),
            BargeInType.MinWordsNoSpeaking => new MinWordsNoSpeakingStrategy(),
            BargeInType.MinWords => new MinWordsBargeInStrategy(),
            _ => throw new ArgumentOutOfRangeException(nameof(options.BargeInType), "Invalid BargeInType")
        };
    }
}

using PersonaEngine.Lib.Live2D.Framework;
using PersonaEngine.Lib.Live2D.Framework.Math;

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     描画クラス
/// </summary>
/// <remarks>
///     コンストラクタ
/// </remarks>
public class LAppView(LAppDelegate lapp)
{
    /// <summary>
    ///     デバイスからスクリーンへの行列
    /// </summary>
    private readonly CubismMatrix44 _deviceToScreen = new();

    /// <summary>
    ///     タッチマネージャー
    /// </summary>
    private readonly TouchManager _touchManager = new();

    /// <summary>
    ///     viewMatrix
    /// </summary>
    private readonly CubismViewMatrix _viewMatrix = new();

    /// <summary>
    ///     初期化する。
    /// </summary>
    public void Initialize()
    {
        lapp.GL.GetWindowSize(out var width, out var height);

        if ( width == 0 || height == 0 )
        {
            return;
        }

        // 縦サイズを基準とする
        var ratio  = (float)width / height;
        var left   = -ratio;
        var right  = ratio;
        var bottom = LAppDefine.ViewLogicalLeft;
        var top    = LAppDefine.ViewLogicalRight;

        _viewMatrix.SetScreenRect(left, right, bottom, top); // デバイスに対応する画面の範囲。 Xの左端, Xの右端, Yの下端, Yの上端
        _viewMatrix.Scale(LAppDefine.ViewScale, LAppDefine.ViewScale);

        _deviceToScreen.LoadIdentity(); // サイズが変わった際などリセット必須
        if ( width > height )
        {
            var screenW = MathF.Abs(right - left);
            _deviceToScreen.ScaleRelative(screenW / width, -screenW / width);
        }
        else
        {
            var screenH = MathF.Abs(top - bottom);
            _deviceToScreen.ScaleRelative(screenH / height, -screenH / height);
        }

        _deviceToScreen.TranslateRelative(-width * 0.5f, -height * 0.5f);

        // 表示範囲の設定
        _viewMatrix.MaxScale = LAppDefine.ViewMaxScale; // 限界拡大率
        _viewMatrix.MinScale = LAppDefine.ViewMinScale; // 限界縮小率

        // 表示できる最大範囲
        _viewMatrix.SetMaxScreenRect(
                                     LAppDefine.ViewLogicalMaxLeft,
                                     LAppDefine.ViewLogicalMaxRight,
                                     LAppDefine.ViewLogicalMaxBottom,
                                     LAppDefine.ViewLogicalMaxTop
                                    );
    }

    /// <summary>
    ///     描画する。
    /// </summary>
    public void Render()
    {
        var Live2DManager = lapp.Live2dManager;
        Live2DManager.ViewMatrix.SetMatrix(_viewMatrix);

        // Cubism更新・描画
        Live2DManager.OnUpdate();
    }

    /// <summary>
    ///     タッチされたときに呼ばれる。
    /// </summary>
    /// <param name="pointX">スクリーンX座標</param>
    /// <param name="pointY">スクリーンY座標</param>
    public void OnTouchesBegan(float pointX, float pointY)
    {
        _touchManager.TouchesBegan(pointX, pointY);
        CubismLog.Debug($"[Live2D]touchesBegan x:{pointX:#.##} y:{pointY:#.##}");
    }

    /// <summary>
    ///     タッチしているときにポインタが動いたら呼ばれる。
    /// </summary>
    /// <param name="pointX">スクリーンX座標</param>
    /// <param name="pointY">スクリーンY座標</param>
    public void OnTouchesMoved(float pointX, float pointY)
    {
        var viewX = TransformViewX(_touchManager.GetX());
        var viewY = TransformViewY(_touchManager.GetY());

        _touchManager.TouchesMoved(pointX, pointY);

        lapp.Live2dManager.OnDrag(viewX, viewY);
    }

    /// <summary>
    ///     タッチが終了したら呼ばれる。
    /// </summary>
    /// <param name="pointX">スクリーンX座標</param>
    /// <param name="pointY">スクリーンY座標</param>
    public void OnTouchesEnded(float _, float __)
    {
        // タッチ終了
        var live2DManager = lapp.Live2dManager;
        live2DManager.OnDrag(0.0f, 0.0f);
        // シングルタップ
        var x = _deviceToScreen.TransformX(_touchManager.GetX()); // 論理座標変換した座標を取得。
        var y = _deviceToScreen.TransformY(_touchManager.GetY()); // 論理座標変換した座標を取得。
        CubismLog.Debug($"[Live2D]touchesEnded x:{x:#.##} y:{y:#.##}");
        live2DManager.OnTap(x, y);
    }

    /// <summary>
    ///     X座標をView座標に変換する。
    /// </summary>
    /// <param name="deviceX">デバイスX座標</param>
    public float TransformViewX(float deviceX)
    {
        var screenX = _deviceToScreen.TransformX(deviceX); // 論理座標変換した座標を取得。

        return _viewMatrix.InvertTransformX(screenX); // 拡大、縮小、移動後の値。
    }

    /// <summary>
    ///     Y座標をView座標に変換する。
    /// </summary>
    /// <param name="deviceY">デバイスY座標</param>
    public float TransformViewY(float deviceY)
    {
        var screenY = _deviceToScreen.TransformY(deviceY); // 論理座標変換した座標を取得。

        return _viewMatrix.InvertTransformY(screenY); // 拡大、縮小、移動後の値。
    }

    /// <summary>
    ///     X座標をScreen座標に変換する。
    /// </summary>
    /// <param name="deviceX">デバイスX座標</param>
    public float TransformScreenX(float deviceX) { return _deviceToScreen.TransformX(deviceX); }

    /// <summary>
    ///     Y座標をScreen座標に変換する。
    /// </summary>
    /// <param name="deviceY">デバイスY座標</param>
    public float TransformScreenY(float deviceY) { return _deviceToScreen.TransformY(deviceY); }

    /// <summary>
    ///     別レンダリングターゲットにモデルを描画するサンプルで
    ///     描画時のαを決定する
    /// </summary>
    public static float GetSpriteAlpha(int assign)
    {
        // assignの数値に応じて適当に決定
        var alpha = 0.25f + assign * 0.5f; // サンプルとしてαに適当な差をつける
        if ( alpha > 1.0f )
        {
            alpha = 1.0f;
        }

        if ( alpha < 0.1f )
        {
            alpha = 0.1f;
        }

        return alpha;
    }
}

using System.Runtime.CompilerServices;
using System.Text;

namespace PersonaEngine.Lib.TTS.Synthesis;

public record Token
{
    public string Text { get; set; } = string.Empty;

    public string Tag { get; set; } = string.Empty;

    public string Whitespace { get; set; } = string.Empty;

    public bool IsHead { get; set; } = true;

    public string? Alias { get; set; }

    public string? Phonemes { get; set; }

    public double? Stress { get; set; }

    public string? Currency { get; set; }

    public string NumFlags { get; set; } = string.Empty;

    public bool Prespace { get; set; }

    public int? Rating { get; set; }

    public double? StartTs { get; set; }

    public double? EndTs { get; set; }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Token MergeTokens(List<Token> tokens, string? unk = null)
    {
        if ( tokens.Count == 0 )
        {
            throw new ArgumentException("Cannot merge empty token list", nameof(tokens));
        }

        if ( tokens.Count == 1 )
        {
            return tokens[0];
        }

        var     stressValues   = new HashSet<double?>();
        var     currencyValues = new HashSet<string?>();
        var     ratingValues   = new HashSet<int?>();
        string? phonemes;

        foreach ( var t in tokens )
        {
            if ( t.Stress.HasValue )
            {
                stressValues.Add(t.Stress);
            }

            if ( t.Currency != null )
            {
                currencyValues.Add(t.Currency);
            }

            ratingValues.Add(t.Rating);
        }

        if ( unk == null )
        {
            phonemes = null;
        }
        else
        {
            var sb = new StringBuilder();
            for ( var i = 0; i < tokens.Count; i++ )
            {
                var t = tokens[i];
                if ( t.Prespace && sb.Length > 0 &&
                     sb[sb.Length - 1] != ' ' &&
                     !string.IsNullOrEmpty(t.Phonemes) )
                {
                    sb.Append(' ');
                }

                sb.Append(t.Phonemes == null ? unk : t.Phonemes);
            }

            phonemes = sb.ToString();
        }

        // Get tag from token with highest "weight"
        var tag       = tokens[0].Tag;
        var maxWeight = GetTagWeight(tokens[0]);

        for ( var i = 1; i < tokens.Count; i++ )
        {
            var weight = GetTagWeight(tokens[i]);
            if ( weight > maxWeight )
            {
                maxWeight = weight;
                tag       = tokens[i].Tag;
            }
        }

        // Concatenate text and whitespace
        var textBuilder = new StringBuilder();
        for ( var i = 0; i < tokens.Count - 1; i++ )
        {
            textBuilder.Append(tokens[i].Text);
            textBuilder.Append(tokens[i].Whitespace);
        }

        textBuilder.Append(tokens[tokens.Count - 1].Text);

        // Build num flags
        var numFlagsBuilder = new StringBuilder();
        var flagsSet        = new HashSet<char>();

        foreach ( var t in tokens )
        {
            foreach ( var c in t.NumFlags )
            {
                flagsSet.Add(c);
            }
        }

        foreach ( var c in flagsSet.OrderBy(c => c) )
        {
            numFlagsBuilder.Append(c);
        }

        return new Token {
                             Text       = textBuilder.ToString(),
                             Tag        = tag,
                             Whitespace = tokens[tokens.Count - 1].Whitespace,
                             IsHead     = tokens[0].IsHead,
                             Alias      = null,
                             Phonemes   = phonemes,
                             Stress     = stressValues.Count == 1 ? stressValues.First() : null,
                             Currency   = currencyValues.Any() ? currencyValues.OrderByDescending(c => c).First() : null,
                             NumFlags   = numFlagsBuilder.ToString(),
                             Prespace   = tokens[0].Prespace,
                             Rating     = ratingValues.Contains(null) ? null : ratingValues.Min(),
                             StartTs    = tokens[0].StartTs,
                             EndTs      = tokens[tokens.Count - 1].EndTs
                         };
    }

    private static int GetTagWeight(Token token)
    {
        var weight = 0;
        foreach ( var c in token.Text )
        {
            weight += char.IsLower(c) ? 1 : 2;
        }

        return weight;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool IsTo()
    {
        return Text == "to" || Text == "To" ||
               (Text == "TO" && (Tag == "TO" || Tag == "IN"));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int StressWeight()
    {
        if ( string.IsNullOrEmpty(Phonemes) )
        {
            return 0;
        }

        var weight = 0;
        foreach ( var c in Phonemes )
        {
            if ( PhonemizerConstants.Diphthongs.Contains(c) )
            {
                weight += 2;
            }
            else
            {
                weight += 1;
            }
        }

        return weight;
    }
}

using Microsoft.Extensions.Logging;

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
///     Implementation of the emotion tracking service
/// </summary>
public class EmotionService : IEmotionService
{
    private readonly Dictionary<Guid, IReadOnlyList<EmotionTiming>> _emotionMap = new();

    private readonly ILogger<EmotionService> _logger;

    public EmotionService(ILogger<EmotionService> logger) { _logger = logger; }

    public void RegisterEmotions(Guid segmentId, IReadOnlyList<EmotionTiming> emotions)
    {
        if (_emotionMap.Count > 100)
        {
            _emotionMap.Clear();
            _logger.LogWarning("Emotion map cleared due to size limit");
        }

        if ( emotions is not { Count: > 0 } )
        {
            return;
        }

        _emotionMap[segmentId] = emotions;
        _logger.LogDebug("Registered {Count} emotions for segment {SegmentId}", emotions.Count, segmentId);
    }

    public IReadOnlyList<EmotionTiming> GetEmotions(Guid segmentId)
    {
        if ( _emotionMap.TryGetValue(segmentId, out var emotions) )
        {
            return emotions;
        }

        return Array.Empty<EmotionTiming>();
    }
}

using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Rendering;

/// <summary>
///     モデル描画を処理するレンダラ
///     サブクラスに環境依存の描画命令を記述する
/// </summary>
public abstract class CubismRenderer : IDisposable
{
    /// <summary>
    ///     Model-View-Projection 行列
    /// </summary>
    private readonly CubismMatrix44 _mvpMatrix4x4 = new();

    public CubismTextureColor ClearColor = new(0, 0, 0, 0);

    /// <summary>
    ///     モデル自体のカラー(RGBA)
    /// </summary>
    public CubismTextureColor ModelColor = new();

    /// <summary>
    ///     レンダラのインスタンスを生成して取得する
    /// </summary>
    public CubismRenderer(CubismModel model)
    {
        _mvpMatrix4x4.LoadIdentity();
        Model = model ?? throw new Exception("model is null");
    }

    /// <summary>
    ///     テクスチャの異方性フィルタリングのパラメータ
    /// </summary>
    public float Anisotropy { get; set; }

    /// <summary>
    ///     レンダリング対象のモデル
    /// </summary>
    public CubismModel Model { get; private set; }

    /// <summary>
    ///     乗算済みαならtrue
    /// </summary>
    public bool IsPremultipliedAlpha { get; set; }

    /// <summary>
    ///     カリングが有効ならtrue
    /// </summary>
    public bool IsCulling { get; set; }

    /// <summary>
    ///     falseの場合、マスクを纏めて描画する trueの場合、マスクはパーツ描画ごとに書き直す
    /// </summary>
    public bool UseHighPrecisionMask { get; set; }

    /// <summary>
    ///     レンダラのインスタンスを解放する
    /// </summary>
    public abstract void Dispose();

    /// <summary>
    ///     モデルを描画する
    /// </summary>
    public void DrawModel()
    {
        /**
         * DoDrawModelの描画前と描画後に以下の関数を呼んでください。
         * ・SaveProfile();
         * ・RestoreProfile();
         * これはレンダラの描画設定を保存・復帰させることで、
         * モデル描画直前の状態に戻すための処理です。
         */

        SaveProfile();

        DoDrawModel();

        RestoreProfile();
    }

    /// <summary>
    ///     Model-View-Projection 行列をセットする
    ///     配列は複製されるので元の配列は外で破棄して良い
    /// </summary>
    /// <param name="matrix4x4">Model-View-Projection 行列</param>
    public void SetMvpMatrix(CubismMatrix44 matrix4x4) { _mvpMatrix4x4.SetMatrix(matrix4x4.Tr); }

    /// <summary>
    ///     Model-View-Projection 行列を取得する
    /// </summary>
    /// <returns>Model-View-Projection 行列</returns>
    public CubismMatrix44 GetMvpMatrix() { return _mvpMatrix4x4; }

    /// <summary>
    ///     透明度を考慮したモデルの色を計算する。
    /// </summary>
    /// <param name="opacity">透明度</param>
    /// <returns>RGBAのカラー情報</returns>
    public CubismTextureColor GetModelColorWithOpacity(float opacity)
    {
        CubismTextureColor modelColorRGBA = new(ModelColor);
        modelColorRGBA.A *= opacity;
        if ( IsPremultipliedAlpha )
        {
            modelColorRGBA.R *= modelColorRGBA.A;
            modelColorRGBA.G *= modelColorRGBA.A;
            modelColorRGBA.B *= modelColorRGBA.A;
        }

        return modelColorRGBA;
    }

    /// <summary>
    ///     モデルの色をセットする。
    ///     各色0.0f～1.0fの間で指定する(1.0fが標準の状態）。
    /// </summary>
    /// <param name="red">赤チャンネルの値</param>
    /// <param name="green">緑チャンネルの値</param>
    /// <param name="blue">青チャンネルの値</param>
    /// <param name="alpha">αチャンネルの値</param>
    public void SetModelColor(float red, float green, float blue, float alpha)
    {
        if ( red < 0.0f )
        {
            red = 0.0f;
        }
        else if ( red > 1.0f )
        {
            red = 1.0f;
        }

        if ( green < 0.0f )
        {
            green = 0.0f;
        }
        else if ( green > 1.0f )
        {
            green = 1.0f;
        }

        if ( blue < 0.0f )
        {
            blue = 0.0f;
        }
        else if ( blue > 1.0f )
        {
            blue = 1.0f;
        }

        if ( alpha < 0.0f )
        {
            alpha = 0.0f;
        }
        else if ( alpha > 1.0f )
        {
            alpha = 1.0f;
        }

        ModelColor.R = red;
        ModelColor.G = green;
        ModelColor.B = blue;
        ModelColor.A = alpha;
    }

    /// <summary>
    ///     モデル描画の実装
    /// </summary>
    protected abstract void DoDrawModel();

    /// <summary>
    ///     描画オブジェクト（アートメッシュ）を描画する。
    ///     ポリゴンメッシュとテクスチャ番号をセットで渡す。
    /// </summary>
    /// <param name="textureNo">描画するテクスチャ番号</param>
    /// <param name="indexCount">描画オブジェクトのインデックス値</param>
    /// <param name="vertexCount">ポリゴンメッシュの頂点数</param>
    /// <param name="indexArray">ポリゴンメッシュ頂点のインデックス配列</param>
    /// <param name="vertexArray">ポリゴンメッシュの頂点配列</param>
    /// <param name="uvArray">uv配列</param>
    /// <param name="opacity">不透明度</param>
    /// <param name="colorBlendMode">カラーブレンディングのタイプ</param>
    /// <param name="invertedMask">マスク使用時のマスクの反転使用</param>
    internal abstract unsafe void DrawMesh(int     textureNo,  int             indexCount,     int    vertexCount
                                         , ushort* indexArray, float*          vertexArray,    float* uvArray
                                         , float   opacity,    CubismBlendMode colorBlendMode, bool   invertedMask);

    /// <summary>
    ///     モデル描画直前のレンダラのステートを保持する
    /// </summary>
    protected abstract void SaveProfile();

    /// <summary>
    ///     モデル描画直前のレンダラのステートを復帰させる
    /// </summary>
    protected abstract void RestoreProfile();
}

using System.Text;

namespace PersonaEngine.Lib.Utils;

public static class TextExtensions
{
    public static string SafeNormalizeUnicode(this string input)
    {
        if ( string.IsNullOrEmpty(input) )
        {
            return input;
        }

        try
        {
            return input.Normalize(NormalizationForm.FormKC);
        }
        catch (ArgumentException)
        {
            // Failed,try to remove invalid unicode chars
        }

        // Remove invalid characters
        var result     = new StringBuilder(input.Length);
        var currentPos = 0;

        while ( currentPos < input.Length )
        {
            var invalidPos = FindInvalidCharIndex(input[currentPos..]);
            if ( invalidPos == -1 )
            {
                // No more invalid characters found, add the rest
                result.Append(input.AsSpan(currentPos));

                break;
            }

            // Add the valid portion before the invalid character
            if ( invalidPos > 0 )
            {
                result.Append(input.AsSpan(currentPos, invalidPos));
            }

            // Skip the invalid character (or pair)
            if ( input[currentPos + invalidPos] >= 0xD800 && input[currentPos + invalidPos] <= 0xDBFF )
            {
                // Skip surrogate pair
                currentPos += invalidPos + 2;
            }
            else
            {
                // Skip single character
                currentPos += invalidPos + 1;
            }
        }

        // Now normalize the cleaned string
        return result.ToString().Normalize(NormalizationForm.FormKC);
    }

    /// <summary>
    ///     Searches invalid charachters (non-chars defined in Unicode standard and invalid surrogate pairs) in a string
    /// </summary>
    /// <param name="aString"> the string to search for invalid chars </param>
    /// <returns>the index of the first bad char or -1 if no bad char is found</returns>
    private static int FindInvalidCharIndex(string aString)
    {
        for ( var i = 0; i < aString.Length; i++ )
        {
            int ch = aString[i];
            if ( ch < 0xD800 ) // char is up to first high surrogate
            {
                continue;
            }

            if ( ch is >= 0xD800 and <= 0xDBFF )
            {
                // found high surrogate -> check surrogate pair
                i++;
                if ( i == aString.Length )
                {
                    // last char is high surrogate, so it is missing its pair
                    return i - 1;
                }

                int chlow = aString[i];
                if ( chlow is < 0xDC00 or > 0xDFFF )
                {
                    // did not found a low surrogate after the high surrogate
                    return i - 1;
                }

                // convert to UTF32 - like in Char.ConvertToUtf32(highSurrogate, lowSurrogate)
                ch = (ch - 0xD800) * 0x400 + (chlow - 0xDC00) + 0x10000;
                if ( ch > 0x10FFFF )
                {
                    // invalid Unicode code point - maximum excedeed
                    return i;
                }

                if ( (ch & 0xFFFE) == 0xFFFE )
                {
                    // other non-char found
                    return i;
                }

                // found a good surrogate pair
                continue;
            }

            if ( ch is >= 0xDC00 and <= 0xDFFF )
            {
                // unexpected low surrogate
                return i;
            }

            if ( ch is >= 0xFDD0 and <= 0xFDEF )
            {
                // non-chars are considered invalid by System.Text.Encoding.GetBytes() and String.Normalize()
                return i;
            }

            if ( (ch & 0xFFFE) == 0xFFFE )
            {
                // other non-char found
                return i;
            }
        }

        return -1;
    }

    public static int GetWordCount(this string text)
    {
        if ( string.IsNullOrWhiteSpace(text) )
        {
            return 0;
        }

        var wordCount = 0;
        var inWord    = false;

        foreach ( var c in text )
        {
            if ( char.IsWhiteSpace(c) )
            {
                inWord = false;
            }
            else
            {
                if ( inWord )
                {
                    continue;
                }

                wordCount++;
                inWord = true;
            }
        }

        return wordCount;
    }
}

using System.Drawing;
using System.Runtime.InteropServices;

using FontStashSharp.Interfaces;

using PersonaEngine.Lib.UI.Common;

using Silk.NET.OpenGL;

using Texture = PersonaEngine.Lib.UI.Common.Texture;

namespace PersonaEngine.Lib.UI.Text.Rendering;

internal class Texture2DManager(GL gl) : ITexture2DManager
{
    public object CreateTexture(int width, int height)
    {
        // Create an empty texture of the specified size
        // Allocate empty memory for the texture (4 bytes per pixel for RGBA format)
        var data = IntPtr.Zero;

        try
        {
            // Allocate unmanaged memory for the texture data
            var size = width * height * 4;
            data = Marshal.AllocHGlobal(size);

            // Initialize with transparent pixels (optional)
            unsafe
            {
                var ptr = (byte*)data.ToPointer();
                for ( var i = 0; i < size; i++ )
                {
                    ptr[i] = 0;
                }
            }

            // Create the texture with the empty data
            // generateMipmaps is set to false as this is for UI/text rendering
            return new Texture(gl, width, height, data);
        }
        finally
        {
            // Free the unmanaged memory after the texture is created
            if ( data != IntPtr.Zero )
            {
                Marshal.FreeHGlobal(data);
            }
        }
    }

    public Point GetTextureSize(object texture)
    {
        var t = (Texture)texture;

        return new Point((int)t.Width, (int)t.Height);
    }

    public void SetTextureData(object texture, Rectangle bounds, byte[] data)
    {
        var t = (Texture)texture;
        t.Bind();

        unsafe
        {
            fixed (byte* ptr = data)
            {
                gl.TexSubImage2D(
                                 GLEnum.Texture2D,
                                 0,                      // mipmap level
                                 bounds.Left,            // x offset
                                 bounds.Top,             // y offset
                                 (uint)bounds.Width,     // width
                                 (uint)bounds.Height,    // height
                                 PixelFormat.Bgra,       // format matching the Texture class
                                 PixelType.UnsignedByte, // data type
                                 ptr                     // data pointer
                                );

                gl.CheckError();
            }
        }
    }
}

using System.Text.Json;

namespace PersonaEngine.Lib.Live2D.Framework.Model;

/// <summary>
///     ユーザデータをロード、管理、検索インターフェイス、解放までを行う。
/// </summary>
public class CubismModelUserData
{
    public const string ArtMesh = "ArtMesh";

    public const string Meta = "Meta";

    public const string UserDataCount = "UserDataCount";

    public const string TotalUserDataSize = "TotalUserDataSize";

    public const string UserData = "UserData";

    public const string Target = "Target";

    public const string Id = "Id";

    public const string Value = "Value";

    /// <summary>
    ///     ユーザデータ構造体配列
    /// </summary>
    private readonly List<CubismModelUserDataNode> _userDataNodes = [];

    /// <summary>
    ///     閲覧リスト保持
    /// </summary>
    public readonly List<CubismModelUserDataNode> ArtMeshUserDataNodes = [];

    /// <summary>
    ///     userdata3.jsonをパースする。
    /// </summary>
    /// <param name="data">userdata3.jsonが読み込まれいるバッファ</param>
    public CubismModelUserData(string data)
    {
        using var stream = File.Open(data, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        var obj = JsonSerializer.Deserialize(stream, CubismModelUserDataObjContext.Default.CubismModelUserDataObj)
                  ?? throw new Exception("Load UserData error");

        var typeOfArtMesh = CubismFramework.CubismIdManager.GetId(ArtMesh);

        var nodeCount = obj.Meta.UserDataCount;

        for ( var i = 0; i < nodeCount; i++ )
        {
            var                     node    = obj.UserData[i];
            CubismModelUserDataNode addNode = new() { TargetId = CubismFramework.CubismIdManager.GetId(node.Id), TargetType = CubismFramework.CubismIdManager.GetId(node.Target), Value = CubismFramework.CubismIdManager.GetId(node.Value) };
            _userDataNodes.Add(addNode);

            if ( addNode.TargetType == typeOfArtMesh )
            {
                ArtMeshUserDataNodes.Add(addNode);
            }
        }
    }
}

using System.Runtime.InteropServices;

using PersonaEngine.Lib.UI.Common;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.Text.Rendering;

public class BufferObject<T> : IDisposable where T : unmanaged
{
    private readonly BufferTargetARB _bufferType;

    private readonly GL _gl;

    private readonly uint _handle;

    private readonly int _size;

    public unsafe BufferObject(GL glApi, int size, BufferTargetARB bufferType, bool isDynamic)
    {
        _gl = glApi;

        _bufferType = bufferType;
        _size       = size;

        _handle = _gl.GenBuffer();
        _gl.CheckError();

        Bind();

        var elementSizeInBytes = Marshal.SizeOf<T>();
        _gl.BufferData(bufferType, (nuint)(size * elementSizeInBytes), null, isDynamic ? BufferUsageARB.StreamDraw : BufferUsageARB.StaticDraw);
        _gl.CheckError();
    }

    public void Dispose()
    {
        _gl.DeleteBuffer(_handle);
        _gl.CheckError();
    }

    public void Bind()
    {
        _gl.BindBuffer(_bufferType, _handle);
        _gl.CheckError();
    }

    public unsafe void SetData(T[] data, int startIndex, int elementCount)
    {
        Bind();

        fixed (T* dataPtr = &data[startIndex])
        {
            var elementSizeInBytes = sizeof(T);

            _gl.BufferSubData(_bufferType, 0, (nuint)(elementCount * elementSizeInBytes), dataPtr);
            _gl.CheckError();
        }
    }
}

using System.Numerics;

namespace PersonaEngine.Lib.Live2D.Framework.Math;

/// <summary>
///     数値計算などに使用するユーティリティクラス
/// </summary>
public static class CubismMath
{
    public const float Pi = 3.1415926535897932384626433832795f;

    public const float Epsilon = 0.00001f;

    /// <summary>
    ///     第一引数の値を最小値と最大値の範囲に収めた値を返す
    /// </summary>
    /// <param name="value">収められる値</param>
    /// <param name="min">範囲の最小値</param>
    /// <param name="max">範囲の最大値</param>
    /// <returns>最小値と最大値の範囲に収めた値</returns>
    public static float RangeF(float value, float min, float max)
    {
        if ( value < min )
        {
            value = min;
        }
        else if ( value > max )
        {
            value = max;
        }

        return value;
    }

    /// <summary>
    ///     イージング処理されたサインを求める
    ///     フェードイン・アウト時のイージングに利用できる
    /// </summary>
    /// <param name="value">イージングを行う値</param>
    /// <returns>イージング処理されたサイン値</returns>
    public static float GetEasingSine(float value)
    {
        if ( value < 0.0f )
        {
            return 0.0f;
        }

        if ( value > 1.0f )
        {
            return 1.0f;
        }

        return 0.5f - 0.5f * MathF.Cos(value * Pi);
    }

    /// <summary>
    ///     大きい方の値を返す。
    /// </summary>
    /// <param name="l">左辺の値</param>
    /// <param name="r">右辺の値</param>
    /// <returns>大きい方の値</returns>
    public static float Max(float l, float r) { return l > r ? l : r; }

    /// <summary>
    ///     小さい方の値を返す。
    /// </summary>
    /// <param name="l">左辺の値</param>
    /// <param name="r">右辺の値</param>
    /// <returns>小さい方の値</returns>
    public static float Min(float l, float r) { return l > r ? r : l; }

    /// <summary>
    ///     角度値をラジアン値に変換します。
    /// </summary>
    /// <param name="degrees">角度値</param>
    /// <returns>角度値から変換したラジアン値</returns>
    public static float DegreesToRadian(float degrees) { return degrees / 180.0f * Pi; }

    /// <summary>
    ///     ラジアン値を角度値に変換します。
    /// </summary>
    /// <param name="radian">ラジアン値</param>
    /// <returns>ラジアン値から変換した角度値</returns>
    public static float RadianToDegrees(float radian) { return radian * 180.0f / Pi; }

    /// <summary>
    ///     2つのベクトルからラジアン値を求めます。
    /// </summary>
    /// <param name="from">始点ベクトル</param>
    /// <param name="to">終点ベクトル</param>
    /// <returns>ラジアン値から求めた方向ベクトル</returns>
    public static float DirectionToRadian(Vector2 from, Vector2 to)
    {
        float q1;
        float q2;
        float ret;

        q1 = MathF.Atan2(to.Y, to.X);
        q2 = MathF.Atan2(from.Y, from.X);

        ret = q1 - q2;

        while ( ret < -Pi )
        {
            ret += Pi * 2.0f;
        }

        while ( ret > Pi )
        {
            ret -= Pi * 2.0f;
        }

        return ret;
    }

    /// <summary>
    ///     2つのベクトルから角度値を求めます。
    /// </summary>
    /// <param name="from">始点ベクトル</param>
    /// <param name="to">終点ベクトル</param>
    /// <returns>角度値から求めた方向ベクトル</returns>
    public static float DirectionToDegrees(Vector2 from, Vector2 to)
    {
        float radian;
        float degree;

        radian = DirectionToRadian(from, to);
        degree = RadianToDegrees(radian);

        if ( to.X - from.X > 0.0f )
        {
            degree = -degree;
        }

        return degree;
    }

    /// <summary>
    ///     ラジアン値を方向ベクトルに変換します。
    /// </summary>
    /// <param name="totalAngle">ラジアン値</param>
    /// <returns>ラジアン値から変換した方向ベクトル</returns>
    public static Vector2 RadianToDirection(float totalAngle) { return new Vector2(MathF.Sin(totalAngle), MathF.Cos(totalAngle)); }

    /// <summary>
    ///     三次方程式の三次項の係数が0になったときに補欠的に二次方程式の解をもとめる。
    ///     a * x^2 + b * x + c = 0
    /// </summary>
    /// <param name="a">二次項の係数値</param>
    /// <param name="b">一次項の係数値</param>
    /// <param name="c">定数項の値</param>
    /// <returns>二次方程式の解</returns>
    public static float QuadraticEquation(float a, float b, float c)
    {
        if ( MathF.Abs(a) < Epsilon )
        {
            if ( MathF.Abs(b) < Epsilon )
            {
                return -c;
            }

            return -c / b;
        }

        return -(b + MathF.Sqrt(b * b - 4.0f * a * c)) / (2.0f * a);
    }

    /// <summary>
    ///     カルダノの公式によってベジェのt値に該当する３次方程式の解を求める。
    ///     重解になったときには0.0～1.0の値になる解を返す。
    ///     a * x^3 + b * x^2 + c * x + d = 0
    /// </summary>
    /// <param name="a">三次項の係数値</param>
    /// <param name="b">二次項の係数値</param>
    /// <param name="c">一次項の係数値</param>
    /// <param name="d">定数項の値</param>
    /// <returns>0.0～1.0の間にある解</returns>
    public static float CardanoAlgorithmForBezier(float a, float b, float c, float d)
    {
        if ( MathF.Abs(a) < Epsilon )
        {
            return RangeF(QuadraticEquation(b, c, d), 0.0f, 1.0f);
        }

        var ba = b / a;
        var ca = c / a;
        var da = d / a;

        var p            = (3.0f * ca - ba * ba) / 3.0f;
        var p3           = p / 3.0f;
        var q            = (2.0f * ba * ba * ba - 9.0f * ba * ca + 27.0f * da) / 27.0f;
        var q2           = q / 2.0f;
        var discriminant = q2 * q2 + p3 * p3 * p3;

        var center    = 0.5f;
        var threshold = center + 0.01f;

        float root1;
        float u1;

        if ( discriminant < 0.0f )
        {
            var mp3    = -p / 3.0f;
            var mp33   = mp3 * mp3 * mp3;
            var r      = MathF.Sqrt(mp33);
            var t      = -q / (2.0f * r);
            var cosphi = RangeF(t, -1.0f, 1.0f);
            var phi    = MathF.Acos(cosphi);
            var crtr   = MathF.Cbrt(r);
            var t1     = 2.0f * crtr;

            root1 = t1 * MathF.Cos(phi / 3.0f) - ba / 3.0f;
            if ( MathF.Abs(root1 - center) < threshold )
            {
                return RangeF(root1, 0.0f, 1.0f);
            }

            var root2 = t1 * MathF.Cos((phi + 2.0f * Pi) / 3.0f) - ba / 3.0f;
            if ( MathF.Abs(root2 - center) < threshold )
            {
                return RangeF(root2, 0.0f, 1.0f);
            }

            var root3 = t1 * MathF.Cos((phi + 4.0f * Pi) / 3.0f) - ba / 3.0f;

            return RangeF(root3, 0.0f, 1.0f);
        }

        if ( discriminant == 0.0f )
        {
            if ( q2 < 0.0f )
            {
                u1 = MathF.Cbrt(-q2);
            }
            else
            {
                u1 = -MathF.Cbrt(q2);
            }

            root1 = 2.0f * u1 - ba / 3.0f;
            if ( MathF.Abs(root1 - center) < threshold )
            {
                return RangeF(root1, 0.0f, 1.0f);
            }

            var root2 = -u1 - ba / 3.0f;

            return RangeF(root2, 0.0f, 1.0f);
        }

        var sd = MathF.Sqrt(discriminant);
        u1 = MathF.Cbrt(sd - q2);
        var v1 = MathF.Cbrt(sd + q2);
        root1 = u1 - v1 - ba / 3.0f;

        return RangeF(root1, 0.0f, 1.0f);
    }

    /// <summary>
    ///     値を範囲内に納めて返す
    /// </summary>
    /// <param name="val">範囲内か確認する値</param>
    /// <param name="min">最小値</param>
    /// <param name="max">最大値</param>
    /// <returns>範囲内に収まった値</returns>
    public static int Clamp(int val, int min, int max)
    {
        if ( val < min )
        {
            return min;
        }

        if ( max < val )
        {
            return max;
        }

        return val;
    }

    /// <summary>
    ///     値を範囲内に納めて返す
    /// </summary>
    /// <param name="val">範囲内か確認する値</param>
    /// <param name="min">最小値</param>
    /// <param name="max">最大値</param>
    /// <returns>範囲内に収まった値</returns>
    public static float ClampF(float val, float min, float max)
    {
        if ( val < min )
        {
            return min;
        }

        if ( max < val )
        {
            return max;
        }

        return val;
    }

    /// <summary>
    ///     浮動小数点の余りを求める。
    /// </summary>
    /// <param name="dividend">被除数（割られる値）</param>
    /// <param name="divisor">除数（割る値）</param>
    /// <returns>余り</returns>
    public static float ModF(float dividend, float divisor)
    {
        if ( !float.IsFinite(dividend) || divisor == 0 || float.IsNaN(dividend) || float.IsNaN(divisor) )
        {
            CubismLog.Warning("dividend: %f, divisor: %f ModF() returns 'NaN'.", dividend, divisor);

            return float.NaN;
        }

        // 絶対値に変換する。
        var absDividend = MathF.Abs(dividend);
        var absDivisor  = MathF.Abs(divisor);

        // 絶対値で割り算する。
        var result = absDividend - MathF.Floor(absDividend / absDivisor) * absDivisor;

        // 符号を被除数のものに指定する。
        return MathF.CopySign(result, dividend);
    }
}

using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

/// <summary>
///     モーションの抽象基底クラス。MotionQueueManagerによってモーションの再生を管理する。
/// </summary>
public abstract class ACubismMotion
{
    protected readonly List<string> FiredEventValues = [];

    /// <summary>
    ///     コンストラクタ。
    /// </summary>
    public ACubismMotion()
    {
        FadeInSeconds  = -1.0f;
        FadeOutSeconds = -1.0f;
        Weight         = 1.0f;
    }

    /// <summary>
    ///     フェードインにかかる時間[秒]
    /// </summary>
    public float FadeInSeconds { get; set; }

    /// <summary>
    ///     フェードアウトにかかる時間[秒]
    /// </summary>
    public float FadeOutSeconds { get; set; }

    /// <summary>
    ///     モーションの重み
    /// </summary>
    public float Weight { get; set; }

    /// <summary>
    ///     モーション再生の開始時刻[秒]
    /// </summary>
    public float OffsetSeconds { get; set; }

    // モーション再生終了コールバック関数
    public FinishedMotionCallback? OnFinishedMotion { get; set; }

    /// <summary>
    ///     モデルのパラメータを更新する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="motionQueueEntry">CubismMotionQueueManagerで管理されているモーション</param>
    /// <param name="userTimeSeconds">デルタ時間の積算値[秒]</param>
    public void UpdateParameters(CubismModel model, CubismMotionQueueEntry motionQueueEntry, float userTimeSeconds)
    {
        if ( !motionQueueEntry.Available || motionQueueEntry.Finished )
        {
            return;
        }

        SetupMotionQueueEntry(motionQueueEntry, userTimeSeconds);

        var fadeWeight = UpdateFadeWeight(motionQueueEntry, userTimeSeconds);

        //---- 全てのパラメータIDをループする ----
        DoUpdateParameters(model, userTimeSeconds, fadeWeight, motionQueueEntry);

        //後処理
        //終了時刻を過ぎたら終了フラグを立てる（CubismMotionQueueManager）
        if ( motionQueueEntry.EndTime > 0 && motionQueueEntry.EndTime < userTimeSeconds )
        {
            motionQueueEntry.Finished = true; //終了
        }
    }

    /// <summary>
    ///     モーションの再生を開始するためのセットアップを行う。
    /// </summary>
    /// <param name="motionQueueEntry">CubismMotionQueueManagerによって管理されるモーション</param>
    /// <param name="userTimeSeconds">総再生時間（秒）</param>
    public void SetupMotionQueueEntry(CubismMotionQueueEntry motionQueueEntry, float userTimeSeconds)
    {
        if ( !motionQueueEntry.Available || motionQueueEntry.Finished )
        {
            return;
        }

        if ( motionQueueEntry.Started )
        {
            return;
        }

        motionQueueEntry.Started         = true;
        motionQueueEntry.StartTime       = userTimeSeconds - OffsetSeconds; //モーションの開始時刻を記録
        motionQueueEntry.FadeInStartTime = userTimeSeconds;                 //フェードインの開始時刻

        var duration = GetDuration();

        if ( motionQueueEntry.EndTime < 0 )
        {
            //開始していないうちに終了設定している場合がある。
            motionQueueEntry.EndTime = duration <= 0 ? -1 : motionQueueEntry.StartTime + duration;
            //duration == -1 の場合はループする
        }
    }

    /// <summary>
    ///     モーションのウェイトを更新する。
    /// </summary>
    /// <param name="motionQueueEntry">CubismMotionQueueManagerで管理されているモーション</param>
    /// <param name="userTimeSeconds">デルタ時間の積算値[秒]</param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    public float UpdateFadeWeight(CubismMotionQueueEntry? motionQueueEntry, float userTimeSeconds)
    {
        if ( motionQueueEntry == null )
        {
            CubismLog.Error("motionQueueEntry is null.");

            return 0;
        }

        var fadeWeight = Weight; //現在の値と掛け合わせる割合

        //---- フェードイン・アウトの処理 ----
        //単純なサイン関数でイージングする
        var fadeIn = FadeInSeconds == 0.0f
                         ? 1.0f
                         : CubismMath.GetEasingSine((userTimeSeconds - motionQueueEntry.FadeInStartTime) / FadeInSeconds);

        var fadeOut = FadeOutSeconds == 0.0f || motionQueueEntry.EndTime < 0.0f
                          ? 1.0f
                          : CubismMath.GetEasingSine((motionQueueEntry.EndTime - userTimeSeconds) / FadeOutSeconds);

        fadeWeight = fadeWeight * fadeIn * fadeOut;

        motionQueueEntry.SetState(userTimeSeconds, fadeWeight);

        if ( 0.0f > fadeWeight || fadeWeight > 1.0f )
        {
            throw new Exception("fadeWeight out of range");
        }

        return fadeWeight;
    }

    /// <summary>
    ///     モーションの長さを取得する。
    ///     ループのときは「-1」。
    ///     ループではない場合は、オーバーライドする。
    ///     正の値の時は取得される時間で終了する。
    ///     「-1」のときは外部から停止命令が無い限り終わらない処理となる。
    /// </summary>
    /// <returns>モーションの長さ[秒]</returns>
    public virtual float GetDuration() { return -1.0f; }

    /// <summary>
    ///     モーションのループ1回分の長さを取得する。
    ///     ループしない場合は GetDuration()と同じ値を返す。
    ///     ループ一回分の長さが定義できない場合（プログラム的に動き続けるサブクラスなど）の場合は「-1」を返す
    /// </summary>
    /// <returns>モーションのループ1回分の長さ[秒]</returns>
    public virtual float GetLoopDuration() { return -1.0f; }

    /// <summary>
    ///     イベント発火のチェック。
    ///     入力する時間は呼ばれるモーションタイミングを０とした秒数で行う。
    /// </summary>
    /// <param name="beforeCheckTimeSeconds">前回のイベントチェック時間[秒]</param>
    /// <param name="motionTimeSeconds">今回の再生時間[秒]</param>
    /// <returns></returns>
    public virtual List<string> GetFiredEvent(float beforeCheckTimeSeconds, float motionTimeSeconds) { return FiredEventValues; }

    /// <summary>
    ///     透明度のカーブが存在するかどうかを確認する
    /// </summary>
    /// <returns>
    ///     true  . キーが存在する
    ///     false . キーが存在しない
    /// </returns>
    public virtual bool IsExistModelOpacity() { return false; }

    /// <summary>
    ///     透明度のカーブのインデックスを返す
    /// </summary>
    /// <returns>success：透明度のカーブのインデックス</returns>
    public virtual int GetModelOpacityIndex() { return -1; }

    /// <summary>
    ///     透明度のIdを返す
    /// </summary>
    /// <returns>透明度のId</returns>
    public virtual string? GetModelOpacityId(int index) { return ""; }

    public virtual float GetModelOpacityValue() { return 1.0f; }

    public abstract void DoUpdateParameters(CubismModel model, float userTimeSeconds, float weight, CubismMotionQueueEntry motionQueueEntry);
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Context;

public class MaxTurnsCleanupStrategy : IConversationCleanupStrategy
{
    private readonly int _maxTurns;

    public MaxTurnsCleanupStrategy(int maxTurns)
    {
        if ( maxTurns < 0 )
        {
            throw new ArgumentOutOfRangeException(nameof(maxTurns), "Max turns cannot be negative.");
        }

        _maxTurns = maxTurns;
    }

    public bool Cleanup(List<InteractionTurn> history, ConversationContextOptions options, IReadOnlyDictionary<string, ParticipantInfo> participants)
    {
        if ( _maxTurns == 0 || history.Count <= _maxTurns )
        {
            return false;
        }

        var turnsToRemove = history.Count - _maxTurns;
        if ( turnsToRemove > 0 )
        {
            history.RemoveRange(0, turnsToRemove);

            return true;
        }

        return false;
    }
}

using Microsoft.Extensions.Logging;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Adapters.Audio.Output;

public class AudioProgressNotifier : IAudioProgressNotifier
{
    private readonly ILogger<AudioProgressNotifier> _logger;

    public AudioProgressNotifier(ILogger<AudioProgressNotifier> logger) { _logger = logger; }

    public event EventHandler<AudioChunkPlaybackStartedEvent>? ChunkPlaybackStarted;

    public event EventHandler<AudioChunkPlaybackEndedEvent>? ChunkPlaybackEnded;

    public event EventHandler<AudioPlaybackProgressEvent>? PlaybackProgress;

    public void RaiseChunkStarted(object? sender, AudioChunkPlaybackStartedEvent args)
    {
        _logger.LogTrace("Raising ChunkPlaybackStarted event for TurnId: {TurnId}, Chunk Id: {Sequence}", args.TurnId, args.Chunk.Id);
        SafeInvoke(ChunkPlaybackStarted, sender, args);
    }

    public void RaiseChunkEnded(object? sender, AudioChunkPlaybackEndedEvent args)
    {
        _logger.LogTrace("Raising ChunkPlaybackEnded event for TurnId: {TurnId}, Chunk Id: {Sequence}", args.TurnId, args.Chunk.Id);
        SafeInvoke(ChunkPlaybackEnded, sender, args);
    }

    public void RaiseProgress(object? sender, AudioPlaybackProgressEvent args)
    {
        SafeInvoke(PlaybackProgress, sender, args);
    }

    private void SafeInvoke<TEventArgs>(EventHandler<TEventArgs>? eventHandler, object? sender, TEventArgs args)
    {
        if ( eventHandler == null )
        {
            return;
        }

        var invocationList = eventHandler.GetInvocationList();

        foreach ( var handlerDelegate in invocationList )
        {
            try
            {
                var handler = (EventHandler<TEventArgs>)handlerDelegate;
                handler(sender, args);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception in audio progress event handler ({EventType}).", typeof(TEventArgs).Name);
            }
        }
    }
}

namespace PersonaEngine.Lib.Live2D.Framework.Math;

/// <summary>
///     4x4行列の便利クラス。
/// </summary>
public record CubismMatrix44
{
    private readonly float[] _mpt1 = [
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    ];

    private readonly float[] _mpt2 = [
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    ];

    private readonly float[] Ident = [
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f
    ];

    public float[] _mpt = new float[16];

    /// <summary>
    ///     4x4行列データ
    /// </summary>
    protected float[] _tr = new float[16];

    /// <summary>
    ///     コンストラクタ。
    /// </summary>
    public CubismMatrix44() { LoadIdentity(); }

    public float[] Tr => _tr;

    /// <summary>
    ///     単位行列に初期化する。
    /// </summary>
    public void LoadIdentity() { SetMatrix(Ident); }

    /// <summary>
    ///     行列を設定する。
    /// </summary>
    /// <param name="tr">16個の浮動小数点数で表される4x4の行列</param>
    public void SetMatrix(float[] tr) { Array.Copy(tr, _tr, 16); }

    public void SetMatrix(CubismMatrix44 tr) { SetMatrix(tr.Tr); }

    /// <summary>
    ///     X軸の拡大率を取得する。
    /// </summary>
    /// <returns>X軸の拡大率</returns>
    public float GetScaleX() { return _tr[0]; }

    /// <summary>
    ///     Y軸の拡大率を取得する。
    /// </summary>
    /// <returns>Y軸の拡大率</returns>
    public float GetScaleY() { return _tr[5]; }

    /// <summary>
    ///     X軸の移動量を取得する。
    /// </summary>
    /// <returns>X軸の移動量</returns>
    public float GetTranslateX() { return _tr[12]; }

    /// <summary>
    ///     Y軸の移動量を取得する。
    /// </summary>
    /// <returns>Y軸の移動量</returns>
    public float GetTranslateY() { return _tr[13]; }

    /// <summary>
    ///     X軸の値を現在の行列で計算する。
    /// </summary>
    /// <param name="src">X軸の値</param>
    /// <returns>現在の行列で計算されたX軸の値</returns>
    public float TransformX(float src) { return _tr[0] * src + _tr[12]; }

    /// <summary>
    ///     Y軸の値を現在の行列で計算する。
    /// </summary>
    /// <param name="src">Y軸の値</param>
    /// <returns>現在の行列で計算されたY軸の値</returns>
    public float TransformY(float src) { return _tr[5] * src + _tr[13]; }

    /// <summary>
    ///     X軸の値を現在の行列で逆計算する。
    /// </summary>
    /// <param name="src">X軸の値</param>
    /// <returns>現在の行列で逆計算されたX軸の値</returns>
    public float InvertTransformX(float src) { return (src - _tr[12]) / _tr[0]; }

    /// <summary>
    ///     Y軸の値を現在の行列で逆計算する。
    /// </summary>
    /// <param name="src">Y軸の値</param>
    /// <returns>現在の行列で逆計算されたY軸の値</returns>
    public float InvertTransformY(float src) { return (src - _tr[13]) / _tr[5]; }

    /// <summary>
    ///     現在の行列の位置を起点にして相対的に移動する。
    /// </summary>
    /// <param name="x">X軸の移動量</param>
    /// <param name="y">Y軸の移動量</param>
    public void TranslateRelative(float x, float y)
    {
        _mpt1[12] = x;
        _mpt1[13] = y;
        MultiplyByMatrix(_mpt1);
    }

    /// <summary>
    ///     現在の行列の位置を指定した位置へ移動する。
    /// </summary>
    /// <param name="x">X軸の移動量</param>
    /// <param name="y">Y軸の移動量</param>
    public void Translate(float x, float y)
    {
        _tr[12] = x;
        _tr[13] = y;
    }

    /// <summary>
    ///     現在の行列のX軸の位置を指定した位置へ移動する。
    /// </summary>
    /// <param name="x">X軸の移動量</param>
    public void TranslateX(float x) { _tr[12] = x; }

    /// <summary>
    ///     現在の行列のY軸の位置を指定した位置へ移動する。
    /// </summary>
    /// <param name="y">Y軸の移動量</param>
    public void TranslateY(float y) { _tr[13] = y; }

    /// <summary>
    ///     現在の行列の拡大率を相対的に設定する。
    /// </summary>
    /// <param name="x">X軸の拡大率</param>
    /// <param name="y">Y軸の拡大率</param>
    public void ScaleRelative(float x, float y)
    {
        _mpt2[0] = x;
        _mpt2[5] = y;
        MultiplyByMatrix(_mpt2);
    }

    /// <summary>
    ///     現在の行列の拡大率を指定した倍率に設定する。
    /// </summary>
    /// <param name="x">X軸の拡大率</param>
    /// <param name="y">Y軸の拡大率</param>
    public void Scale(float x, float y)
    {
        _tr[0] = x;
        _tr[5] = y;
    }

    public void MultiplyByMatrix(float[] a)
    {
        Array.Fill(_mpt, 0);

        var n = 4;

        for ( var i = 0; i < n; ++i )
        {
            for ( var j = 0; j < n; ++j )
            {
                for ( var k = 0; k < n; ++k )
                {
                    _mpt[j + i * 4] += a[k + i * 4] * _tr[j + k * 4];
                }
            }
        }

        Array.Copy(_mpt, _tr, 16);
    }

    /// <summary>
    ///     現在の行列に行列を乗算する。
    /// </summary>
    /// <param name="m">行列</param>
    public void MultiplyByMatrix(CubismMatrix44 m) { MultiplyByMatrix(m.Tr); }
}

using System.Globalization;

using PersonaEngine.Lib.Configuration;

using Whisper.net;

namespace PersonaEngine.Lib.ASR.Transcriber;

public sealed class WhisperSpeechTranscriptorFactory : ISpeechTranscriptorFactory
{
    private readonly WhisperProcessorBuilder builder;

    private readonly WhisperFactory? whisperFactory;

    public WhisperSpeechTranscriptorFactory(WhisperFactory factory, bool dispose = true)
    {
        builder = factory.CreateBuilder();
        if ( dispose )
        {
            whisperFactory = factory;
        }
    }

    public WhisperSpeechTranscriptorFactory(WhisperProcessorBuilder builder) { this.builder = builder; }

    public WhisperSpeechTranscriptorFactory(string modelFileName)
    {
        whisperFactory = WhisperFactory.FromPath(modelFileName);
        builder        = whisperFactory.CreateBuilder();
    }

    public ISpeechTranscriptor Create(SpeechTranscriptorOptions options)
    {
        var currentBuilder = builder;
        if ( options.Prompt != null )
        {
            currentBuilder = currentBuilder.WithPrompt(options.Prompt);
        }

        if ( options.LanguageAutoDetect )
        {
            currentBuilder = currentBuilder.WithLanguage("auto");
        }
        else
        {
            currentBuilder = currentBuilder.WithLanguage(ToWhisperLanguage(options.Language));
        }

        if ( options.RetrieveTokenDetails )
        {
            currentBuilder = currentBuilder.WithTokenTimestamps();
        }

        if ( options.Template != null )
        {
            currentBuilder = currentBuilder.ApplyTemplate(options.Template.Value);
        }

        var processor = currentBuilder.Build();

        return new WhisperNetSpeechTranscriptor(processor);
    }

    public void Dispose() { whisperFactory?.Dispose(); }

    private static string ToWhisperLanguage(CultureInfo languageCode)
    {
        if ( !WhisperNetSupportedLanguage.IsSupported(languageCode) )
        {
            throw new NotSupportedException($"The language provided as: {languageCode.ThreeLetterISOLanguageName} is not supported by Whisper.net.");
        }

        return languageCode.TwoLetterISOLanguageName;
    }
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Implements notification service
/// </summary>
public class NotificationService : INotificationService
{
    private readonly List<Notification> _activeNotifications = new();

    public void ShowInfo(string message, Action? action = null, string actionLabel = "OK") { AddNotification(new Notification(Notification.NotificationType.Info, message, 5f, action, actionLabel)); }

    public void ShowSuccess(string message, Action? action = null, string actionLabel = "OK") { AddNotification(new Notification(Notification.NotificationType.Success, message, 5f, action, actionLabel)); }

    public void ShowWarning(string message, Action? action = null, string actionLabel = "OK") { AddNotification(new Notification(Notification.NotificationType.Warning, message, 8f, action, actionLabel)); }

    public void ShowError(string message, Action? action = null, string actionLabel = "OK") { AddNotification(new Notification(Notification.NotificationType.Error, message, 10f, action, actionLabel)); }

    public IReadOnlyList<Notification> GetActiveNotifications() { return _activeNotifications.AsReadOnly(); }

    public void Update(float deltaTime)
    {
        // Update remaining time for each notification
        for ( var i = _activeNotifications.Count - 1; i >= 0; i-- )
        {
            var notification = _activeNotifications[i];
            notification.RemainingTime -= deltaTime;

            // Remove expired notifications
            if ( notification.RemainingTime <= 0 )
            {
                _activeNotifications.RemoveAt(i);
            }
        }
    }

    private void AddNotification(Notification notification)
    {
        // Limit the number of active notifications
        if ( _activeNotifications.Count >= 5 )
        {
            _activeNotifications.RemoveAt(0);
        }

        _activeNotifications.Add(notification);
    }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Voice data for synthesis
/// </summary>
public class VoiceData
{
    private const int StyleDim = 256;

    private readonly ReadOnlyMemory<float> _rawEmbedding;

    public VoiceData(string id, ReadOnlyMemory<float> rawEmbedding)
    {
        Id            = id;
        _rawEmbedding = rawEmbedding;
    }

    /// <summary>
    ///     Voice ID
    /// </summary>
    public string Id { get; }

    public ReadOnlyMemory<float> GetEmbedding(ReadOnlySpan<int> inputDimensions)
    {
        var numTokens = GetNumTokens(inputDimensions);
        var offset    = numTokens * StyleDim;

        // Check bounds to prevent access violations
        return offset + StyleDim > _rawEmbedding.Length
                   ?
                   // Return empty vector if out of bounds
                   new float[StyleDim]
                   :
                   // Return the slice of memory at the specified offset
                   _rawEmbedding.Slice(offset, StyleDim);
    }

    private int GetNumTokens(ReadOnlySpan<int> inputDimensions)
    {
        var lastDim = inputDimensions[^1];

        return Math.Min(Math.Max(lastDim - 2, 0), 509);
    }
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

/// <summary>
///     オフスクリーン描画用構造体
/// </summary>
public class CubismOffscreenSurface_OpenGLES2(OpenGLApi gl)
{
    /// <summary>
    ///     引数によって設定されたカラーバッファか？
    /// </summary>
    private bool _isColorBufferInherited;

    /// <summary>
    ///     旧フレームバッファ
    /// </summary>
    private int _oldFBO;

    /// <summary>
    ///     レンダリングターゲットとしてのアドレス
    /// </summary>
    public int RenderTexture { get; private set; }

    /// <summary>
    ///     描画の際使用するテクスチャとしてのアドレス
    /// </summary>
    public int ColorBuffer { get; private set; }

    /// <summary>
    ///     Create時に指定された幅
    /// </summary>
    public int BufferWidth { get; private set; }

    /// <summary>
    ///     Create時に指定された高さ
    /// </summary>
    public int BufferHeight { get; private set; }

    /// <summary>
    ///     指定の描画ターゲットに向けて描画開始
    /// </summary>
    /// <param name="restoreFBO">0以上の場合、EndDrawでこの値をglBindFramebufferする</param>
    public void BeginDraw(int restoreFBO = -1)
    {
        if ( RenderTexture == 0 )
        {
            return;
        }

        // バックバッファのサーフェイスを記憶しておく
        if ( restoreFBO < 0 )
        {
            gl.GetIntegerv(gl.GL_FRAMEBUFFER_BINDING, out _oldFBO);
        }
        else
        {
            _oldFBO = restoreFBO;
        }

        //マスク用RenderTextureをactiveにセット
        gl.BindFramebuffer(gl.GL_FRAMEBUFFER, RenderTexture);
    }

    /// <summary>
    ///     描画終了
    /// </summary>
    public void EndDraw()
    {
        if ( RenderTexture == 0 )
        {
            return;
        }

        // 描画対象を戻す
        gl.BindFramebuffer(gl.GL_FRAMEBUFFER, _oldFBO);
    }

    /// <summary>
    ///     レンダリングターゲットのクリア
    ///     呼ぶ場合はBeginDrawの後で呼ぶこと
    /// </summary>
    /// <param name="r">赤(0.0~1.0)</param>
    /// <param name="g">緑(0.0~1.0)</param>
    /// <param name="b">青(0.0~1.0)</param>
    /// <param name="a">α(0.0~1.0)</param>
    public void Clear(float r, float g, float b, float a)
    {
        // マスクをクリアする
        gl.ClearColor(r, g, b, a);
        gl.Clear(gl.GL_COLOR_BUFFER_BIT);
    }

    /// <summary>
    ///     CubismOffscreenFrame作成
    /// </summary>
    /// <param name="displayBufferWidth">作成するバッファ幅</param>
    /// <param name="displayBufferHeight">作成するバッファ高さ</param>
    /// <param name="colorBuffer">0以外の場合、ピクセル格納領域としてcolorBufferを使用する</param>
    public bool CreateOffscreenSurface(int displayBufferWidth, int displayBufferHeight, int colorBuffer = 0)
    {
        // 一旦削除
        DestroyOffscreenSurface();

        // 新しく生成する
        if ( colorBuffer == 0 )
        {
            ColorBuffer = gl.GenTexture();

            gl.BindTexture(gl.GL_TEXTURE_2D, ColorBuffer);
            gl.TexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, displayBufferWidth, displayBufferHeight, 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, 0);
            gl.TexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE);
            gl.TexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE);
            gl.TexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR);
            gl.TexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR);
            gl.BindTexture(gl.GL_TEXTURE_2D, 0);

            _isColorBufferInherited = false;
        }
        else
        {
            // 指定されたものを使用
            ColorBuffer = colorBuffer;

            _isColorBufferInherited = true;
        }

        gl.GetIntegerv(gl.GL_FRAMEBUFFER_BINDING, out var tmpFramebufferObject);

        var ret = gl.GenFramebuffer();
        gl.BindFramebuffer(gl.GL_FRAMEBUFFER, ret);
        gl.FramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, ColorBuffer, 0);
        gl.BindFramebuffer(gl.GL_FRAMEBUFFER, tmpFramebufferObject);

        RenderTexture = ret;

        BufferWidth  = displayBufferWidth;
        BufferHeight = displayBufferHeight;

        // 成功
        return true;
    }

    /// <summary>
    ///     CubismOffscreenFrameの削除
    /// </summary>
    public void DestroyOffscreenSurface()
    {
        if ( !_isColorBufferInherited && ColorBuffer != 0 )
        {
            gl.DeleteTexture(ColorBuffer);
            ColorBuffer = 0;
        }

        if ( RenderTexture != 0 )
        {
            gl.DeleteFramebuffer(RenderTexture);
            RenderTexture = 0;
        }
    }

    /// <summary>
    ///     現在有効かどうか
    /// </summary>
    public bool IsValid() { return RenderTexture != 0; }
}

using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Text;

namespace PersonaEngine.Lib.TTS.Synthesis;

public static class LexiconUtils
{
    private static readonly ConcurrentDictionary<string, string> _parentTagCache = new(StringComparer.Ordinal);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string? GetParentTag(string? tag)
    {
        if ( tag == null )
        {
            return null;
        }

        if ( _parentTagCache.TryGetValue(tag, out var cachedParent) )
        {
            return cachedParent;
        }

        string parent;
        if ( tag.StartsWith("VB") )
        {
            parent = "VERB";
        }
        else if ( tag.StartsWith("NN") )
        {
            parent = "NOUN";
        }
        else if ( tag.StartsWith("ADV") || tag.StartsWith("RB") )
        {
            parent = "ADV";
        }
        else if ( tag.StartsWith("ADJ") || tag.StartsWith("JJ") )
        {
            parent = "ADJ";
        }
        else
        {
            parent = tag;
        }

        _parentTagCache[tag] = parent;

        return parent;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string Capitalize(ReadOnlySpan<char> str)
    {
        if ( str.IsEmpty )
        {
            return string.Empty;
        }

        // Fast path for single-character strings
        if ( str.Length == 1 )
        {
            return char.ToUpper(str[0]).ToString();
        }

        // Avoid allocation if already capitalized
        if ( char.IsUpper(str[0]) )
        {
            return str.ToString();
        }

        var result = new StringBuilder(str.Length);
        result.Append(char.ToUpper(str[0]));
        result.Append(str.Slice(1));

        return result.ToString();
    }
}

namespace PersonaEngine.Lib.Live2D.Framework.Math;

/// <summary>
///     モデル座標設定用の4x4行列クラス。
/// </summary>
public record CubismModelMatrix : CubismMatrix44
{
    public const string KeyWidth = "width";

    public const string KeyHeight = "height";

    public const string KeyX = "x";

    public const string KeyY = "y";

    public const string KeyCenterX = "center_x";

    public const string KeyCenterY = "center_y";

    public const string KeyTop = "top";

    public const string KeyBottom = "bottom";

    public const string KeyLeft = "left";

    public const string KeyRight = "right";

    /// <summary>
    ///     縦幅
    /// </summary>
    private readonly float _height;

    /// <summary>
    ///     横幅
    /// </summary>
    private readonly float _width;

    public CubismModelMatrix(float w, float h)
    {
        _width  = w;
        _height = h;

        SetHeight(2.0f);
    }

    /// <summary>
    ///     横幅を設定する。
    /// </summary>
    /// <param name="w">横幅</param>
    public void SetWidth(float w)
    {
        var scaleX = w / _width;
        var scaleY = scaleX;
        Scale(scaleX, scaleY);
    }

    /// <summary>
    ///     縦幅を設定する。
    /// </summary>
    /// <param name="h">縦幅</param>
    public void SetHeight(float h)
    {
        var scaleX = h / _height;
        var scaleY = scaleX;
        Scale(scaleX, scaleY);
    }

    /// <summary>
    ///     位置を設定する。
    /// </summary>
    /// <param name="x">X軸の位置</param>
    /// <param name="y">Y軸の位置</param>
    public void SetPosition(float x, float y) { Translate(x, y); }

    /// <summary>
    ///     中心位置を設定する。
    /// </summary>
    /// <param name="x">X軸の中心位置</param>
    /// <param name="y">Y軸の中心位置</param>
    public void SetCenterPosition(float x, float y)
    {
        CenterX(x);
        CenterY(y);
    }

    /// <summary>
    ///     上辺の位置を設定する。
    /// </summary>
    /// <param name="y">上辺のY軸位置</param>
    public void Top(float y) { SetY(y); }

    /// <summary>
    ///     下辺の位置を設定する。
    /// </summary>
    /// <param name="y">下辺のY軸位置</param>
    public void Bottom(float y)
    {
        var h = _height * GetScaleY();
        TranslateY(y - h);
    }

    /// <summary>
    ///     左辺の位置を設定する。
    /// </summary>
    /// <param name="x">左辺のX軸位置</param>
    public void Left(float x) { SetX(x); }

    /// <summary>
    ///     右辺の位置を設定する。
    /// </summary>
    /// <param name="x">右辺のX軸位置</param>
    public void Right(float x)
    {
        var w = _width * GetScaleX();
        TranslateX(x - w);
    }

    /// <summary>
    ///     X軸の中心位置を設定する。
    /// </summary>
    /// <param name="x">X軸の中心位置</param>
    public void CenterX(float x)
    {
        var w = _width * GetScaleX();
        TranslateX(x - w / 2.0f);
    }

    /// <summary>
    ///     X軸の位置を設定する。
    /// </summary>
    /// <param name="x">X軸の位置</param>
    public void SetX(float x) { TranslateX(x); }

    /// <summary>
    ///     Y軸の中心位置を設定する。
    /// </summary>
    /// <param name="y">Y軸の中心位置</param>
    public void CenterY(float y)
    {
        var h = _height * GetScaleY();
        TranslateY(y - h / 2.0f);
    }

    /// <summary>
    ///     Y軸の位置を設定する。
    /// </summary>
    /// <param name="y">Y軸の位置</param>
    public void SetY(float y) { TranslateY(y); }

    /// <summary>
    ///     レイアウト情報から位置を設定する。
    /// </summary>
    /// <param name="layout">レイアウト情報</param>
    public void SetupFromLayout(Dictionary<string, float> layout)
    {
        foreach ( var item in layout )
        {
            if ( item.Key == KeyWidth )
            {
                SetWidth(item.Value);
            }
            else if ( item.Key == KeyHeight )
            {
                SetHeight(item.Value);
            }
        }

        foreach ( var item in layout )
        {
            if ( item.Key == KeyX )
            {
                SetX(item.Value);
            }
            else if ( item.Key == KeyY )
            {
                SetY(item.Value);
            }
            else if ( item.Key == KeyCenterX )
            {
                CenterX(item.Value);
            }
            else if ( item.Key == KeyCenterY )
            {
                CenterY(item.Value);
            }
            else if ( item.Key == KeyTop )
            {
                Top(item.Value);
            }
            else if ( item.Key == KeyBottom )
            {
                Bottom(item.Value);
            }
            else if ( item.Key == KeyLeft )
            {
                Left(item.Value);
            }
            else if ( item.Key == KeyRight )
            {
                Right(item.Value);
            }
        }
    }
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Base implementation for configuration section editors
/// </summary>
public abstract class ConfigSectionEditorBase : IConfigSectionEditor, IDisposable
{
    protected readonly IUiConfigurationManager ConfigManager;

    protected readonly IEditorStateManager StateManager;

    protected internal bool _hasUnsavedChanges = false;

    protected ConfigSectionEditorBase(
        IUiConfigurationManager configManager,
        IEditorStateManager     stateManager)
    {
        ConfigManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
        StateManager  = stateManager ?? throw new ArgumentNullException(nameof(stateManager));
    }

    public abstract string SectionKey { get; }

    public abstract string DisplayName { get; }

    public bool HasUnsavedChanges => _hasUnsavedChanges;

    public virtual void Initialize() { }

    public abstract void Render();

    public virtual void RenderMenuItems() { }

    public virtual void Update(float deltaTime) { }

    public virtual void OnConfigurationChanged(ConfigurationChangedEventArgs args)
    {
        if ( args.Type is ConfigurationChangedEventArgs.ChangeType.Saved or ConfigurationChangedEventArgs.ChangeType.Reloaded )
        {
            _hasUnsavedChanges = false;
        }
    }

    public virtual void Dispose()
    {
        // Base implementation does nothing
        GC.SuppressFinalize(this);
    }

    protected void MarkAsChanged()
    {
        if ( !_hasUnsavedChanges )
        {
            _hasUnsavedChanges = true;
            StateManager.MarkAsChanged(SectionKey);
        }
    }

    protected void MarkAsSaved()
    {
        _hasUnsavedChanges = false;
        StateManager.MarkAsSaved();
    }
}

namespace PersonaEngine.Lib.Live2D.Framework.Math;

/// <summary>
///     カメラの位置変更に使うと便利な4x4行列のクラス。
/// </summary>
public record CubismViewMatrix : CubismMatrix44
{
    /// <summary>
    ///     デバイスに対応する論理座標上の範囲（左辺X軸位置）
    /// </summary>
    public float ScreenLeft { get; private set; }

    /// <summary>
    ///     デバイスに対応する論理座標上の範囲（右辺X軸位置）
    /// </summary>
    public float ScreenRight { get; private set; }

    /// <summary>
    ///     デバイスに対応する論理座標上の範囲（下辺Y軸位置）
    /// </summary>
    public float ScreenTop { get; private set; }

    /// <summary>
    ///     デバイスに対応する論理座標上の範囲（上辺Y軸位置）
    /// </summary>
    public float ScreenBottom { get; private set; }

    /// <summary>
    ///     論理座標上の移動可能範囲（左辺X軸位置）
    /// </summary>
    public float MaxLeft { get; private set; }

    /// <summary>
    ///     論理座標上の移動可能範囲（右辺X軸位置）
    /// </summary>
    public float MaxRight { get; private set; }

    /// <summary>
    ///     論理座標上の移動可能範囲（下辺Y軸位置）
    /// </summary>
    public float MaxTop { get; private set; }

    /// <summary>
    ///     論理座標上の移動可能範囲（上辺Y軸位置）
    /// </summary>
    public float MaxBottom { get; private set; }

    /// <summary>
    ///     拡大率の最大値
    /// </summary>
    public float MaxScale { get; set; }

    /// <summary>
    ///     拡大率の最小値
    /// </summary>
    public float MinScale { get; set; }

    /// <summary>
    ///     移動を調整する。
    /// </summary>
    /// <param name="x">X軸の移動量</param>
    /// <param name="y">Y軸の移動量</param>
    public void AdjustTranslate(float x, float y)
    {
        if ( _tr[0] * MaxLeft + (_tr[12] + x) > ScreenLeft )
        {
            x = ScreenLeft - _tr[0] * MaxLeft - _tr[12];
        }

        if ( _tr[0] * MaxRight + (_tr[12] + x) < ScreenRight )
        {
            x = ScreenRight - _tr[0] * MaxRight - _tr[12];
        }

        if ( _tr[5] * MaxTop + (_tr[13] + y) < ScreenTop )
        {
            y = ScreenTop - _tr[5] * MaxTop - _tr[13];
        }

        if ( _tr[5] * MaxBottom + (_tr[13] + y) > ScreenBottom )
        {
            y = ScreenBottom - _tr[5] * MaxBottom - _tr[13];
        }

        float[] tr1 = [
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            x, y, 0.0f, 1.0f
        ];

        MultiplyByMatrix(tr1);
    }

    /// <summary>
    ///     拡大率を調整する。
    /// </summary>
    /// <param name="cx">拡大を行うX軸の中心位置</param>
    /// <param name="cy">拡大を行うY軸の中心位置</param>
    /// <param name="scale">拡大率</param>
    public void AdjustScale(float cx, float cy, float scale)
    {
        var maxScale = MaxScale;
        var minScale = MinScale;

        var targetScale = scale * _tr[0]; //

        if ( targetScale < minScale )
        {
            if ( _tr[0] > 0.0f )
            {
                scale = minScale / _tr[0];
            }
        }
        else if ( targetScale > maxScale )
        {
            if ( _tr[0] > 0.0f )
            {
                scale = maxScale / _tr[0];
            }
        }

        MultiplyByMatrix([
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            -cx, -cy, 0.0f, 1.0f
        ]);

        MultiplyByMatrix([
            scale, 0.0f, 0.0f, 0.0f,
            0.0f, scale, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f
        ]);

        MultiplyByMatrix([
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            cx, cy, 0.0f, 1.0f
        ]);
    }

    /// <summary>
    ///     デバイスに対応する論理座標上の範囲の設定を行う。
    /// </summary>
    /// <param name="left">左辺のX軸の位置</param>
    /// <param name="right">右辺のX軸の位置</param>
    /// <param name="bottom">下辺のY軸の位置</param>
    /// <param name="top">上辺のY軸の位置</param>
    public void SetScreenRect(float left, float right, float bottom, float top)
    {
        ScreenLeft   = left;
        ScreenRight  = right;
        ScreenTop    = top;
        ScreenBottom = bottom;
    }

    /// <summary>
    ///     デバイスに対応する論理座標上の移動可能範囲の設定を行う。
    /// </summary>
    /// <param name="left">左辺のX軸の位置</param>
    /// <param name="right">右辺のX軸の位置</param>
    /// <param name="bottom">下辺のY軸の位置</param>
    /// <param name="top">上辺のY軸の位置</param>
    public void SetMaxScreenRect(float left, float right, float bottom, float top)
    {
        MaxLeft   = left;
        MaxRight  = right;
        MaxTop    = top;
        MaxBottom = bottom;
    }

    /// <summary>
    ///     拡大率が最大になっているかどうかを確認する。
    /// </summary>
    /// <returns>
    ///     true    拡大率は最大になっている
    ///     false   拡大率は最大になっていない
    /// </returns>
    public bool IsMaxScale() { return GetScaleX() >= MaxScale; }

    /// <summary>
    ///     拡大率が最小になっているかどうかを確認する。
    /// </summary>
    /// <returns>
    ///     true    拡大率は最小になっている
    ///     false   拡大率は最小になっていない
    /// </returns>
    public bool IsMinScale() { return GetScaleX() <= MinScale; }
}

using System.Collections.Concurrent;
using System.Numerics;

using FontStashSharp;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Measures text dimensions using a specific font and caches the results.
///     Provides information about available rendering width and line height.
///     Thread-safe for use in concurrent processing.
/// </summary>
public class TextMeasurer
{
    private readonly ConcurrentDictionary<string, Vector2> _cache = new();

    private readonly float _sideMargin;

    private int _viewportHeight;

    private int _viewportWidth;

    public TextMeasurer(DynamicSpriteFont font, float sideMargin, int initialWidth, int initialHeight)
    {
        Font        = font;
        _sideMargin = sideMargin;
        UpdateViewport(initialWidth, initialHeight);
        LineHeight = Font.MeasureString("Ay").Y;
        if ( LineHeight <= 0 )
        {
            LineHeight = Font.FontSize;
        }
    }

    public float AvailableWidth { get; private set; }

    public float LineHeight { get; private set; }

    public DynamicSpriteFont Font { get; }

    public void UpdateViewport(int width, int height)
    {
        _viewportWidth  = width;
        _viewportHeight = height;
        AvailableWidth  = Math.Max(1, _viewportWidth - 2 * _sideMargin);
        _cache.Clear();
    }

    public Vector2 MeasureText(string text)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return Vector2.Zero;
        }

        return _cache.GetOrAdd(text, t => Font.MeasureString(t));
    }
}

using Whisper.net;

namespace PersonaEngine.Lib.Configuration;

public record AsrConfiguration
{
    public WhisperConfigTemplate TtsMode { get; init; } = WhisperConfigTemplate.Performant;

    public string TtsPrompt { get; init; } = string.Empty;

    public float VadThreshold { get; init; } = 0.5f;

    public float VadThresholdGap { get; init; } = 0.15f;

    public float VadMinSpeechDuration { get; init; } = 250f;

    public float VadMinSilenceDuration { get; init; } = 200f;
}

public enum WhisperConfigTemplate
{
    Performant,

    Balanced,

    Precise
}

public static class WhisperConfigTemplateExtensions
{
    public static WhisperProcessorBuilder ApplyTemplate(this WhisperProcessorBuilder builder, WhisperConfigTemplate template)
    {
        switch ( template )
        {
            case WhisperConfigTemplate.Performant:
                var sampleBuilderA = (GreedySamplingStrategyBuilder)builder.WithGreedySamplingStrategy();
                sampleBuilderA.WithBestOf(1);

                builder = sampleBuilderA.ParentBuilder;
                builder.WithStringPool();

                break;
            case WhisperConfigTemplate.Balanced:
                var sampleBuilderB = (BeamSearchSamplingStrategyBuilder)builder.WithBeamSearchSamplingStrategy();
                sampleBuilderB.WithBeamSize(2);
                sampleBuilderB.WithPatience(1f);

                builder = sampleBuilderB.ParentBuilder;
                builder.WithStringPool();
                builder.WithTemperature(0.0f);

                break;
            case WhisperConfigTemplate.Precise:
                var sampleBuilderC = (BeamSearchSamplingStrategyBuilder)builder.WithBeamSearchSamplingStrategy();
                sampleBuilderC.WithBeamSize(5);
                sampleBuilderC.WithPatience(1f);

                builder = sampleBuilderC.ParentBuilder;
                builder.WithStringPool();
                builder.WithTemperature(0.0f);

                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(template), template, null);
        }

        return builder;
    }
}

namespace PersonaEngine.Lib.Configuration;

public record TtsConfiguration
{
    public string ModelDirectory { get; init; } = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "Models");

    public string EspeakPath { get; init; } = "espeak-ng";

    public KokoroVoiceOptions Voice { get; init; } = new();

    public RVCFilterOptions Rvc { get; init; } = new();
}

public record KokoroVoiceOptions
{
    public string DefaultVoice { get; init; } = "af_heart";

    public bool UseBritishEnglish { get; init; } = false;

    public float DefaultSpeed { get; init; } = 1.0f;

    public int MaxPhonemeLength { get; init; } = 510;

    public int SampleRate { get; init; } = 24000;

    public bool TrimSilence { get; init; } = false;
}

using System.Collections.Concurrent;
using System.ComponentModel;

using PersonaEngine.Lib.Utils;

namespace PersonaEngine.Lib;

internal static class ModelUtils
{
    public static string GetModelPath(ModelType modelType)
    {
        var enumDescription = modelType.GetDescription();
        var fullPath        = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "Models", $"{enumDescription}");

        // Check file or dir exists
        if ( !Path.Exists(fullPath) )
        {
            throw new ApplicationException($"For {modelType} path {fullPath} doesn't exist");
        }

        return fullPath;
    }
}

internal static class PromptUtils
{
    private static readonly ConcurrentDictionary<string, string> PromptCache = new();

    private static string GetModelPath(string filename)
    {
        var fullPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "Prompts", filename);

        if ( !File.Exists(fullPath) )
        {
            throw new ApplicationException($"Prompt file '{filename}' not found at path: {fullPath}");
        }

        return fullPath;
    }

    public static bool TryGetPrompt(string filename, out string? prompt)
    {
        if ( PromptCache.TryGetValue(filename, out prompt) )
        {
            return true;
        }

        try
        {
            var fullPath      = GetModelPath(filename);
            var promptContent = File.ReadAllText(fullPath);
            PromptCache.AddOrUpdate(filename, promptContent, (_, _) => promptContent);
            prompt = promptContent;

            return true;
        }
        catch
        {
            return false;
        }
    }

    public static void ClearCache() { PromptCache.Clear(); }
}

public enum ModelType
{
    [Description("silero_vad_v5.onnx")] Silero,

    [Description("ggml-large-v3-turbo.bin")]
    WhisperGgmlTurbov3,

    [Description("ggml-tiny.en.bin")] WhisperGgmlTiny,

    [Description("badwords.txt")] BadWords,

    [Description("tiny_toxic_detector.onnx")]
    TinyToxic,

    [Description("tiny_toxic_detector_vocab.txt")]
    TinyToxicVocab
}

namespace PersonaEngine.Lib.TTS.Synthesis;

public record AudioSegment
{
    public AudioSegment(Memory<float> audioData, int sampleRate, IReadOnlyList<Token> tokens)
    {
        AudioData  = audioData;
        SampleRate = sampleRate;
        Channels   = 1;
        Tokens     = tokens ?? Array.Empty<Token>();
    }

    public Guid Id { get; init; } = Guid.NewGuid();

    public Memory<float> AudioData { get; set; }

    public int SampleRate { get; set; }

    public float DurationInSeconds => AudioData.Length / (float)SampleRate;

    public IReadOnlyList<Token> Tokens { get; }

    public int Channels { get; }
}

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.LLM;

public class NameTextFilter : ITextFilter
{
    public int Priority => 9999;

    public ValueTask<TextFilterResult> ProcessAsync(string text, CancellationToken cancellationToken = default)
    {
        if ( string.IsNullOrEmpty(text) )
        {
            return ValueTask.FromResult(new TextFilterResult { ProcessedText = text ?? string.Empty });
        }

        var span = text.AsSpan();

        if ( span.Length <= 2 || span[0] != '[' )
        {
            return ValueTask.FromResult(new TextFilterResult { ProcessedText = text });
        }

        var closingBracketIndex = span.IndexOf(']');

        if ( closingBracketIndex <= 1 )
        {
            return ValueTask.FromResult(new TextFilterResult { ProcessedText = text });
        }

        var remainingSpan = span[(closingBracketIndex + 1)..];

        remainingSpan = remainingSpan.TrimStart();

        var processedText = remainingSpan.ToString();

        return ValueTask.FromResult(new TextFilterResult { ProcessedText = processedText });
    }

    public ValueTask PostProcessAsync(TextFilterResult textFilterResult, AudioSegment segment, CancellationToken cancellationToken = default) { return ValueTask.CompletedTask; }
}

namespace PersonaEngine.Lib.Live2D.App;

public class TouchManager
{
    /// <summary>
    ///     前回の値から今回の値へのxの移動距離。
    /// </summary>
    private float _deltaX;

    /// <summary>
    ///     前回の値から今回の値へのyの移動距離。
    /// </summary>
    private float _deltaY;

    /// <summary>
    ///     フリップが有効かどうか
    /// </summary>
    private bool _flipAvailable;

    /// <summary>
    ///     2本以上でタッチしたときの指の距離
    /// </summary>
    private float _lastTouchDistance;

    /// <summary>
    ///     シングルタッチ時のxの値
    /// </summary>
    private float _lastX;

    /// <summary>
    ///     ダブルタッチ時の一つ目のxの値
    /// </summary>
    private float _lastX1;

    /// <summary>
    ///     ダブルタッチ時の二つ目のxの値
    /// </summary>
    private float _lastX2;

    /// <summary>
    ///     シングルタッチ時のyの値
    /// </summary>
    private float _lastY;

    /// <summary>
    ///     ダブルタッチ時の一つ目のyの値
    /// </summary>
    private float _lastY1;

    /// <summary>
    ///     ダブルタッチ時の二つ目のyの値
    /// </summary>
    private float _lastY2;

    /// <summary>
    ///     このフレームで掛け合わせる拡大率。拡大操作中以外は1。
    /// </summary>
    private float _scale;

    /// <summary>
    ///     タッチを開始した時のyの値
    /// </summary>
    private float _startX;

    /// <summary>
    ///     タッチを開始した時のxの値
    /// </summary>
    private float _startY;

    /// <summary>
    ///     シングルタッチ時はtrue
    /// </summary>
    private bool _touchSingle;

    public TouchManager() { _scale = 1.0f; }

    public float GetCenterX() { return _lastX; }

    public float GetCenterY() { return _lastY; }

    public float GetDeltaX() { return _deltaX; }

    public float GetDeltaY() { return _deltaY; }

    public float GetStartX() { return _startX; }

    public float GetStartY() { return _startY; }

    public float GetScale() { return _scale; }

    public float GetX() { return _lastX; }

    public float GetY() { return _lastY; }

    public float GetX1() { return _lastX1; }

    public float GetY1() { return _lastY1; }

    public float GetX2() { return _lastX2; }

    public float GetY2() { return _lastY2; }

    public bool IsSingleTouch() { return _touchSingle; }

    public bool IsFlickAvailable() { return _flipAvailable; }

    public void DisableFlick() { _flipAvailable = false; }

    /// <summary>
    ///     タッチ開始時イベント
    /// </summary>
    /// <param name="deviceX">タッチした画面のyの値</param>
    /// <param name="deviceY">タッチした画面のxの値</param>
    public void TouchesBegan(float deviceX, float deviceY)
    {
        _lastX             = deviceX;
        _lastY             = deviceY;
        _startX            = deviceX;
        _startY            = deviceY;
        _lastTouchDistance = -1.0f;
        _flipAvailable     = true;
        _touchSingle       = true;
    }

    /// <summary>
    ///     ドラッグ時のイベント
    /// </summary>
    /// <param name="deviceX">タッチした画面のxの値</param>
    /// <param name="deviceY">タッチした画面のyの値</param>
    public void TouchesMoved(float deviceX, float deviceY)
    {
        _lastX             = deviceX;
        _lastY             = deviceY;
        _lastTouchDistance = -1.0f;
        _touchSingle       = true;
    }

    /// <summary>
    ///     ドラッグ時のイベント
    /// </summary>
    /// <param name="deviceX1">1つめのタッチした画面のxの値</param>
    /// <param name="deviceY1">1つめのタッチした画面のyの値</param>
    /// <param name="deviceX2">2つめのタッチした画面のxの値</param>
    /// <param name="deviceY2">2つめのタッチした画面のyの値</param>
    public void TouchesMoved(float deviceX1, float deviceY1, float deviceX2, float deviceY2)
    {
        var distance = CalculateDistance(deviceX1, deviceY1, deviceX2, deviceY2);
        var centerX  = (deviceX1 + deviceX2) * 0.5f;
        var centerY  = (deviceY1 + deviceY2) * 0.5f;

        if ( _lastTouchDistance > 0.0f )
        {
            _scale  = MathF.Pow(distance / _lastTouchDistance, 0.75f);
            _deltaX = CalculateMovingAmount(deviceX1 - _lastX1, deviceX2 - _lastX2);
            _deltaY = CalculateMovingAmount(deviceY1 - _lastY1, deviceY2 - _lastY2);
        }
        else
        {
            _scale  = 1.0f;
            _deltaX = 0.0f;
            _deltaY = 0.0f;
        }

        _lastX             = centerX;
        _lastY             = centerY;
        _lastX1            = deviceX1;
        _lastY1            = deviceY1;
        _lastX2            = deviceX2;
        _lastY2            = deviceY2;
        _lastTouchDistance = distance;
        _touchSingle       = false;
    }

    /// <summary>
    ///     フリックの距離測定
    /// </summary>
    /// <returns>フリック距離</returns>
    public float GetFlickDistance() { return CalculateDistance(_startX, _startY, _lastX, _lastY); }

    /// <summary>
    ///     点1から点2への距離を求める
    /// </summary>
    /// <param name="x1">1つめのタッチした画面のxの値</param>
    /// <param name="y1">1つめのタッチした画面のyの値</param>
    /// <param name="x2">2つめのタッチした画面のxの値</param>
    /// <param name="y2">2つめのタッチした画面のyの値</param>
    /// <returns>2点の距離</returns>
    public static float CalculateDistance(float x1, float y1, float x2, float y2) { return MathF.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); }

    /// <summary>
    ///     二つの値から、移動量を求める。
    ///     違う方向の場合は移動量０。同じ方向の場合は、絶対値が小さい方の値を参照する
    /// </summary>
    /// <param name="v1">1つめの移動量</param>
    /// <param name="v2">2つめの移動量</param>
    /// <returns>小さい方の移動量</returns>
    public static float CalculateMovingAmount(float v1, float v2)
    {
        if ( v1 > 0.0f != v2 > 0.0f )
        {
            return 0.0f;
        }

        var sign           = v1 > 0.0f ? 1.0f : -1.0f;
        var absoluteValue1 = MathF.Abs(v1);
        var absoluteValue2 = MathF.Abs(v2);

        return sign * (absoluteValue1 < absoluteValue2 ? absoluteValue1 : absoluteValue2);
    }
}

using System.Numerics;

using FontStashSharp;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Implements a "pop" animation effect using an overshoot easing function.
/// </summary>
public class PopAnimator : IWordAnimator
{
    private readonly float _overshoot;

    public PopAnimator(float overshoot = 1.70158f) { _overshoot = overshoot; }

    public Vector2 CalculateScale(float progress)
    {
        progress = Math.Clamp(progress, 0.0f, 1.0f);

        var scale = 1.0f + (_overshoot + 1.0f) * MathF.Pow(progress - 1.0f, 3) + _overshoot * MathF.Pow(progress - 1.0f, 2);

        scale = Math.Max(0.0f, scale);

        return new Vector2(scale, scale);
    }

    public FSColor CalculateColor(FSColor startColor, FSColor endColor, float progress)
    {
        progress = Math.Clamp(progress, 0.0f, 1.0f);

        var r = (byte)(startColor.R + (endColor.R - startColor.R) * progress);
        var g = (byte)(startColor.G + (endColor.G - startColor.G) * progress);
        var b = (byte)(startColor.B + (endColor.B - startColor.B) * progress);
        var a = (byte)(startColor.A + (endColor.A - startColor.A) * progress);

        return new FSColor(r, g, b, a);
    }
}

namespace PersonaEngine.Lib.Utils;

public sealed class AsyncQueue<T> : IDisposable
{
    private readonly Queue<T> _queue = new();

    private readonly SemaphoreSlim _semaphore = new(0);

    private bool _isDisposed;

    public void Dispose()
    {
        if ( _isDisposed )
        {
            return;
        }

        _semaphore.Dispose();
        _isDisposed = true;
    }

    public void Enqueue(T item)
    {
        lock (_queue)
        {
            _queue.Enqueue(item);
            _semaphore.Release();
        }
    }

    public async Task<T> DequeueAsync(CancellationToken ct = default)
    {
        await _semaphore.WaitAsync(ct);

        lock (_queue)
        {
            return _queue.Dequeue();
        }
    }
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Effect;

/// <summary>
///     呼吸機能を提供する。
/// </summary>
public class CubismBreath
{
    /// <summary>
    ///     積算時間[秒]
    /// </summary>
    private float _currentTime;

    /// <summary>
    ///     呼吸にひもづいているパラメータのリスト
    /// </summary>
    public required List<BreathParameterData> Parameters { get; init; }

    /// <summary>
    ///     モデルのパラメータを更新する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="deltaTimeSeconds">デルタ時間[秒]</param>
    public void UpdateParameters(CubismModel model, float deltaTimeSeconds)
    {
        _currentTime += deltaTimeSeconds;

        var t = _currentTime * 2.0f * 3.14159f;

        foreach ( var item in Parameters )
        {
            model.AddParameterValue(item.ParameterId, item.Offset +
                                                      item.Peak * MathF.Sin(t / item.Cycle), item.Weight);
        }
    }
}

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Represents a processed audio segment, broken down into lines and words
///     with calculated timing and layout information ready for the timeline.
/// </summary>
public class SubtitleSegment(AudioSegment originalAudioSegment, float absoluteStartTime, string fullText)
{
    public AudioSegment OriginalAudioSegment { get; } = originalAudioSegment;

    public float AbsoluteStartTime { get; } = absoluteStartTime;

    public string FullText { get; } = fullText;

    public List<SubtitleLine> Lines { get; } = new();

    public float EstimatedEndTime { get; private set; } = absoluteStartTime;

    public void AddLine(SubtitleLine line)
    {
        Lines.Add(line);
        if ( line.Words.Count <= 0 )
        {
            return;
        }

        var lastWord = line.Words[^1];
        EstimatedEndTime = Math.Max(EstimatedEndTime, lastWord.AbsoluteStartTime + lastWord.Duration);
    }

    public void Clear()
    {
        Lines.Clear();
        EstimatedEndTime = AbsoluteStartTime;
    }
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents the format of audio data.
/// </summary>
public readonly struct AudioFormat
{
    /// <summary>
    ///     The number of channels in the audio.
    /// </summary>
    public readonly ushort Channels;

    /// <summary>
    ///     The number of bits per sample.
    /// </summary>
    public readonly ushort BitsPerSample;

    /// <summary>
    ///     The sample rate in Hz.
    /// </summary>
    public readonly uint SampleRate;

    /// <summary>
    ///     Creates a new audio format.
    /// </summary>
    public AudioFormat(ushort channels, ushort bitsPerSample, uint sampleRate)
    {
        Channels      = channels;
        BitsPerSample = bitsPerSample;
        SampleRate    = sampleRate;
    }

    /// <summary>
    ///     Gets the number of bytes per sample.
    /// </summary>
    public int BytesPerSample => BitsPerSample / 8;

    /// <summary>
    ///     Gets the number of bytes per frame (a frame contains one sample for each channel).
    /// </summary>
    public int BytesPerFrame => BytesPerSample * Channels;

    /// <summary>
    ///     Creates a mono format with the specified bits per sample and sample rate.
    /// </summary>
    public static AudioFormat CreateMono(ushort bitsPerSample, uint sampleRate) { return new AudioFormat(1, bitsPerSample, sampleRate); }

    /// <summary>
    ///     Creates a stereo format with the specified bits per sample and sample rate.
    /// </summary>
    public static AudioFormat CreateStereo(ushort bitsPerSample, uint sampleRate) { return new AudioFormat(2, bitsPerSample, sampleRate); }
}

using PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.Live2D;

public class SilkNetApi : OpenGLApi
{
    private readonly GL Gl;

    private readonly int Height;

    private readonly int Width;

    public SilkNetApi(GL gl, int width, int height)
    {
        Width  = width;
        Height = height;
        Gl     = gl;
    }

    public override bool IsES2 => false;

    public override bool IsPhoneES2 => false;

    public override bool AlwaysClear => false;

    public override void GetWindowSize(out int w, out int h)
    {
        w = Width;
        h = Height;
    }

    public override void ActiveTexture(int bit) { Gl.ActiveTexture((TextureUnit)bit); }

    public override void AttachShader(int program, int shader) { Gl.AttachShader((uint)program, (uint)shader); }

    public override void BindBuffer(int target, int buffer) { Gl.BindBuffer((BufferTargetARB)target, (uint)buffer); }

    public override void BindFramebuffer(int target, int framebuffer) { Gl.BindFramebuffer((FramebufferTarget)target, (uint)framebuffer); }

    public override void BindTexture(int target, int texture) { Gl.BindTexture((TextureTarget)target, (uint)texture); }

    public override void BindVertexArrayOES(int array) { Gl.BindVertexArray((uint)array); }

    public override void BlendFunc(int sfactor, int dfactor) { Gl.BlendFunc((BlendingFactor)sfactor, (BlendingFactor)dfactor); }

    public override void BlendFuncSeparate(int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) { Gl.BlendFuncSeparate((BlendingFactor)srcRGB, (BlendingFactor)dstRGB, (BlendingFactor)srcAlpha, (BlendingFactor)dstAlpha); }

    public override void Clear(int mask) { Gl.Clear((ClearBufferMask)mask); }

    public override void ClearColor(float r, float g, float b, float a) { Gl.ClearColor(r, g, b, a); }

    public override void ClearDepthf(float depth) { Gl.ClearDepth(depth); }

    public override void ColorMask(bool r, bool g, bool b, bool a) { Gl.ColorMask(r, g, b, a); }

    public override void CompileShader(int shader) { Gl.CompileShader((uint)shader); }

    public override int CreateProgram() { return (int)Gl.CreateProgram(); }

    public override int CreateShader(int type) { return (int)Gl.CreateShader((ShaderType)type); }

    public override void DeleteFramebuffer(int framebuffer) { Gl.DeleteFramebuffer((uint)framebuffer); }

    public override void DeleteProgram(int program) { Gl.DeleteProgram((uint)program); }

    public override void DeleteShader(int shader) { Gl.DeleteShader((uint)shader); }

    public override void DeleteTexture(int texture) { Gl.DeleteTexture((uint)texture); }

    public override void DetachShader(int program, int shader) { Gl.DetachShader((uint)program, (uint)shader); }

    public override void Disable(int cap) { Gl.Disable((EnableCap)cap); }

    public override void DisableVertexAttribArray(int index) { Gl.DisableVertexAttribArray((uint)index); }

    public override void DrawElements(int mode, int count, int type, nint indices)
    {
        unsafe
        {
            Gl.DrawElements((PrimitiveType)mode, (uint)count, (DrawElementsType)type, (void*)indices);
        }
    }

    public override void Enable(int cap) { Gl.Enable((EnableCap)cap); }

    public override void EnableVertexAttribArray(int index) { Gl.EnableVertexAttribArray((uint)index); }

    public override void FramebufferTexture2D(int target, int attachment, int textarget, int texture, int level)
    {
        Gl.FramebufferTexture2D((FramebufferTarget)target, (FramebufferAttachment)attachment,
                                (TextureTarget)textarget, (uint)texture, level);
    }

    public override void FrontFace(int mode) { Gl.FrontFace((FrontFaceDirection)mode); }

    public override void GenerateMipmap(int target) { Gl.GenerateMipmap((TextureTarget)target); }

    public override int GenFramebuffer() { return (int)Gl.GenFramebuffer(); }

    public override int GenTexture() { return (int)Gl.GenTexture(); }

    public override int GetAttribLocation(int program, string name) { return Gl.GetAttribLocation((uint)program, name); }

    public override void GetBooleanv(int pname, bool[] data) { Gl.GetBoolean((GetPName)pname, out data[0]); }

    public override void GetIntegerv(int pname, out int data) { Gl.GetInteger((GetPName)pname, out data); }

    public override void GetIntegerv(int pname, int[] data) { Gl.GetInteger((GetPName)pname, out data[0]); }

    public override void GetProgramInfoLog(int program, out string infoLog)
    {
        var length = Gl.GetProgram((uint)program, GLEnum.InfoLogLength);
        infoLog = Gl.GetProgramInfoLog((uint)program);
    }

    public override unsafe void GetProgramiv(int program, int pname, int* length) { Gl.GetProgram((uint)program, (GLEnum)pname, length); }

    public override void GetShaderInfoLog(int shader, out string infoLog)
    {
        var length = Gl.GetShader((uint)shader, GLEnum.InfoLogLength);
        infoLog = Gl.GetShaderInfoLog((uint)shader);
    }

    public override unsafe void GetShaderiv(int shader, int pname, int* length) { Gl.GetShader((uint)shader, (GLEnum)pname, length); }

    public override int GetUniformLocation(int program, string name) { return Gl.GetUniformLocation((uint)program, name); }

    public override void GetVertexAttribiv(int index, int pname, out int @params) { Gl.GetVertexAttrib((uint)index, (VertexAttribPropertyARB)pname, out @params); }

    public override bool IsEnabled(int cap) { return Gl.IsEnabled((EnableCap)cap); }

    public override void LinkProgram(int program) { Gl.LinkProgram((uint)program); }

    public override void ShaderSource(int shader, string source) { Gl.ShaderSource((uint)shader, source); }

    public override void TexImage2D(int target, int level, int  internalformat, int width, int height, int border,
                                    int format, int type,  nint pixels)
    {
        unsafe
        {
            Gl.TexImage2D((TextureTarget)target, level, (InternalFormat)internalformat,
                          (uint)width, (uint)height, border, (PixelFormat)format,
                          (PixelType)type, pixels == 0 ? null : (void*)pixels);
        }
    }

    public override void TexParameterf(int target, int pname, float param) { Gl.TexParameter((TextureTarget)target, (TextureParameterName)pname, param); }

    public override void TexParameteri(int target, int pname, int param) { Gl.TexParameter((TextureTarget)target, (TextureParameterName)pname, param); }

    public override void Uniform1i(int location, int v0) { Gl.Uniform1(location, v0); }

    public override void Uniform4f(int location, float v0, float v1, float v2, float v3) { Gl.Uniform4(location, v0, v1, v2, v3); }

    public override void UniformMatrix4fv(int location, int count, bool transpose, float[] value) { Gl.UniformMatrix4(location, (uint)count, transpose, value); }

    public override void UseProgram(int program) { Gl.UseProgram((uint)program); }

    public override void ValidateProgram(int program) { Gl.ValidateProgram((uint)program); }

    public override void VertexAttribPointer(int index,  int  size, int type, bool normalized,
                                             int stride, nint pointer)
    {
        unsafe
        {
            Gl.VertexAttribPointer((uint)index, size, (VertexAttribPointerType)type,
                                   normalized, (uint)stride, (void*)pointer);
        }
    }

    public override void Viewport(int x, int y, int width, int height) { Gl.Viewport(x, y, (uint)width, (uint)height); }

    public override int GetError() { return (int)Gl.GetError(); }

    public override int GenBuffer() { return (int)Gl.GenBuffer(); }

    public override void BufferData(int target, int size, nint data, int usage)
    {
        unsafe
        {
            Gl.BufferData((BufferTargetARB)target, (nuint)size, (void*)data, (BufferUsageARB)usage);
        }
    }

    public override int GenVertexArray() { return (int)Gl.GenVertexArray(); }

    public override void BindVertexArray(int array) { Gl.BindVertexArray((uint)array); }
}

using Hexa.NET.ImGui;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Implements UI theming management
/// </summary>
public class UiThemeManager : IUiThemeManager
{
    private UiTheme _currentTheme = new();

    public void ApplyTheme()
    {
        var style = ImGui.GetStyle();

        style.Colors[(int)ImGuiCol.Text]                  = _currentTheme.TextColor;
        style.Colors[(int)ImGuiCol.TextDisabled]          = _currentTheme.TextDisabledColor;
        style.Colors[(int)ImGuiCol.WindowBg]              = _currentTheme.WindowBgColor;
        style.Colors[(int)ImGuiCol.ChildBg]               = _currentTheme.ChildBgColor;
        style.Colors[(int)ImGuiCol.PopupBg]               = _currentTheme.PopupBgColor;
        style.Colors[(int)ImGuiCol.Border]                = _currentTheme.BorderColor;
        style.Colors[(int)ImGuiCol.BorderShadow]          = _currentTheme.BorderShadowColor;
        style.Colors[(int)ImGuiCol.FrameBg]               = _currentTheme.FrameBgColor;
        style.Colors[(int)ImGuiCol.FrameBgHovered]        = _currentTheme.FrameBgHoveredColor;
        style.Colors[(int)ImGuiCol.FrameBgActive]         = _currentTheme.FrameBgActiveColor;
        style.Colors[(int)ImGuiCol.TitleBg]               = _currentTheme.TitleBgColor;
        style.Colors[(int)ImGuiCol.TitleBgActive]         = _currentTheme.TitleBgActiveColor;
        style.Colors[(int)ImGuiCol.TitleBgCollapsed]      = _currentTheme.TitleBgCollapsedColor;
        style.Colors[(int)ImGuiCol.MenuBarBg]             = _currentTheme.MenuBarBgColor;
        style.Colors[(int)ImGuiCol.ScrollbarBg]           = _currentTheme.ScrollbarBgColor;
        style.Colors[(int)ImGuiCol.ScrollbarGrab]         = _currentTheme.ScrollbarGrabColor;
        style.Colors[(int)ImGuiCol.ScrollbarGrabHovered]  = _currentTheme.ScrollbarGrabHoveredColor;
        style.Colors[(int)ImGuiCol.ScrollbarGrabActive]   = _currentTheme.ScrollbarGrabActiveColor;
        style.Colors[(int)ImGuiCol.CheckMark]             = _currentTheme.CheckMarkColor;
        style.Colors[(int)ImGuiCol.SliderGrab]            = _currentTheme.SliderGrabColor;
        style.Colors[(int)ImGuiCol.SliderGrabActive]      = _currentTheme.SliderGrabActiveColor;
        style.Colors[(int)ImGuiCol.Button]                = _currentTheme.ButtonColor;
        style.Colors[(int)ImGuiCol.ButtonHovered]         = _currentTheme.ButtonHoveredColor;
        style.Colors[(int)ImGuiCol.ButtonActive]          = _currentTheme.ButtonActiveColor;
        style.Colors[(int)ImGuiCol.Header]                = _currentTheme.HeaderColor;
        style.Colors[(int)ImGuiCol.HeaderHovered]         = _currentTheme.HeaderHoveredColor;
        style.Colors[(int)ImGuiCol.HeaderActive]          = _currentTheme.HeaderActiveColor;
        style.Colors[(int)ImGuiCol.Separator]             = _currentTheme.SeparatorColor;
        style.Colors[(int)ImGuiCol.SeparatorHovered]      = _currentTheme.SeparatorHoveredColor;
        style.Colors[(int)ImGuiCol.SeparatorActive]       = _currentTheme.SeparatorActiveColor;
        style.Colors[(int)ImGuiCol.ResizeGrip]            = _currentTheme.ResizeGripColor;
        style.Colors[(int)ImGuiCol.ResizeGripHovered]     = _currentTheme.ResizeGripHoveredColor;
        style.Colors[(int)ImGuiCol.ResizeGripActive]      = _currentTheme.ResizeGripActiveColor;
        style.Colors[(int)ImGuiCol.Tab]                   = _currentTheme.TabColor;
        style.Colors[(int)ImGuiCol.TabHovered]            = _currentTheme.TabHoveredColor;
        style.Colors[(int)ImGuiCol.TabSelected]           = _currentTheme.TabActiveColor;
        style.Colors[(int)ImGuiCol.TabDimmed]             = _currentTheme.TabUnfocusedColor;
        style.Colors[(int)ImGuiCol.TabDimmedSelected]     = _currentTheme.TabUnfocusedActiveColor;
        style.Colors[(int)ImGuiCol.PlotLines]             = _currentTheme.PlotLinesColor;
        style.Colors[(int)ImGuiCol.PlotLinesHovered]      = _currentTheme.PlotLinesHoveredColor;
        style.Colors[(int)ImGuiCol.PlotHistogram]         = _currentTheme.PlotHistogramColor;
        style.Colors[(int)ImGuiCol.PlotHistogramHovered]  = _currentTheme.PlotHistogramHoveredColor;
        style.Colors[(int)ImGuiCol.TableHeaderBg]         = _currentTheme.TableHeaderBgColor;
        style.Colors[(int)ImGuiCol.TableBorderStrong]     = _currentTheme.TableBorderStrongColor;
        style.Colors[(int)ImGuiCol.TableBorderLight]      = _currentTheme.TableBorderLightColor;
        style.Colors[(int)ImGuiCol.TableRowBg]            = _currentTheme.TableRowBgColor;
        style.Colors[(int)ImGuiCol.TableRowBgAlt]         = _currentTheme.TableRowBgAltColor;
        style.Colors[(int)ImGuiCol.TextSelectedBg]        = _currentTheme.TextSelectedBgColor;
        style.Colors[(int)ImGuiCol.DragDropTarget]        = _currentTheme.DragDropTargetColor;
        style.Colors[(int)ImGuiCol.NavWindowingHighlight] = _currentTheme.NavWindowingHighlightColor;
        style.Colors[(int)ImGuiCol.NavWindowingDimBg]     = _currentTheme.NavWindowingDimBgColor;
        style.Colors[(int)ImGuiCol.ModalWindowDimBg]      = _currentTheme.ModalWindowDimBgColor;

        // Set style properties
        style.Alpha            = _currentTheme.Alpha;
        style.DisabledAlpha    = _currentTheme.DisabledAlpha;
        style.WindowRounding   = _currentTheme.WindowRounding;
        style.WindowBorderSize = _currentTheme.WindowBorderSize;
        style.WindowMinSize    = _currentTheme.WindowMinSize;
        style.WindowTitleAlign = _currentTheme.WindowTitleAlign;

        // Handle WindowMenuButtonPosition
        if ( _currentTheme.WindowMenuButtonPosition == "Left" )
        {
            style.WindowMenuButtonPosition = ImGuiDir.Left;
        }
        else if ( _currentTheme.WindowMenuButtonPosition == "Right" )
        {
            style.WindowMenuButtonPosition = ImGuiDir.Right;
        }

        style.ChildRounding                    = _currentTheme.ChildRounding;
        style.ChildBorderSize                  = _currentTheme.ChildBorderSize;
        style.PopupRounding                    = _currentTheme.PopupRounding;
        style.PopupBorderSize                  = _currentTheme.PopupBorderSize;
        style.FrameRounding                    = _currentTheme.FrameRounding;
        style.FrameBorderSize                  = _currentTheme.FrameBorderSize;
        style.ItemSpacing                      = _currentTheme.ItemSpacing;
        style.ItemInnerSpacing                 = _currentTheme.ItemInnerSpacing;
        style.CellPadding                      = _currentTheme.CellPadding;
        style.IndentSpacing                    = _currentTheme.IndentSpacing;
        style.ColumnsMinSpacing                = _currentTheme.ColumnsMinSpacing;
        style.ScrollbarSize                    = _currentTheme.ScrollbarSize;
        style.ScrollbarRounding                = _currentTheme.ScrollbarRounding;
        style.GrabMinSize                      = _currentTheme.GrabMinSize;
        style.GrabRounding                     = _currentTheme.GrabRounding;
        style.TabRounding                      = _currentTheme.TabRounding;
        style.TabBorderSize                    = _currentTheme.TabBorderSize;
        style.TabCloseButtonMinWidthSelected   = _currentTheme.TabCloseButtonMinWidthSelected;
        style.TabCloseButtonMinWidthUnselected = _currentTheme.TabCloseButtonMinWidthUnselected;

        // Handle ColorButtonPosition
        if ( _currentTheme.ColorButtonPosition == "Left" )
        {
            style.ColorButtonPosition = ImGuiDir.Left;
        }
        else if ( _currentTheme.ColorButtonPosition == "Right" )
        {
            style.ColorButtonPosition = ImGuiDir.Right;
        }

        style.ButtonTextAlign     = _currentTheme.ButtonTextAlign;
        style.SelectableTextAlign = _currentTheme.SelectableTextAlign;
        style.WindowPadding       = _currentTheme.WindowPadding;
        style.FramePadding        = _currentTheme.FramePadding;
    }

    public void SetTheme(UiTheme theme)
    {
        _currentTheme = theme;
        ApplyTheme();
    }

    public UiTheme GetCurrentTheme() { return _currentTheme; }
}

using System.Collections.Concurrent;

using Serilog.Core;
using Serilog.Events;

namespace PersonaEngine.App;

public class GuidToEmojiEnricher : ILogEventEnricher
{
    private readonly GuidEmojiMapper _mapper;

    public GuidToEmojiEnricher() { _mapper = new GuidEmojiMapper(); }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        var guidProperties = logEvent.Properties
                                     .Where(kvp => kvp.Value is ScalarValue { Value: Guid })
                                     .Select(kvp => new { kvp.Key, Value = (Guid)(((ScalarValue)kvp.Value).Value ?? Guid.Empty) })
                                     .ToList();

        foreach ( var propInfo in guidProperties )
        {
            var emoji = _mapper.GetEmojiForGuid(propInfo.Value);

            logEvent.RemovePropertyIfPresent(propInfo.Key);

            var emojiProperty = propertyFactory.CreateProperty(propInfo.Key, emoji);
            logEvent.AddOrUpdateProperty(emojiProperty);
        }
    }
}

public class GuidEmojiMapper
{
    private static readonly IReadOnlyList<string> Emojis = new List<string> {
                                                                                "🚀",
                                                                                "🌟",
                                                                                "💡",
                                                                                "🔧",
                                                                                "🐞",
                                                                                "🔗",
                                                                                "💾",
                                                                                "🔑",
                                                                                "🎉",
                                                                                "🎯",
                                                                                "📁",
                                                                                "📄",
                                                                                "📦",
                                                                                "🧭",
                                                                                "📡",
                                                                                "🧪",
                                                                                "🧬",
                                                                                "⚙",
                                                                                "💎",
                                                                                "🧩",
                                                                                "🐙",
                                                                                "🦄",
                                                                                "🐘",
                                                                                "🦋",
                                                                                "🐌",
                                                                                "🐬",
                                                                                "🐿",
                                                                                "🍄",
                                                                                "🌵",
                                                                                "🍀"
                                                                            }.AsReadOnly();

    private readonly ConcurrentDictionary<Guid, string> _guidEmojiMap = new();

    private int _emojiIndex = -1;

    public string GetEmojiForGuid(Guid guid)
    {
        if ( guid == Guid.Empty )
        {
            return "\u2796";
        }

        return _guidEmojiMap.GetOrAdd(guid, _ =>
                                            {
                                                var nextIndex      = Interlocked.Increment(ref _emojiIndex);
                                                var emojiListIndex = (nextIndex & int.MaxValue) % Emojis.Count;

                                                return Emojis[emojiListIndex];
                                            });
    }
}

using PersonaEngine.Lib.Live2D.Framework.Core;
using PersonaEngine.Lib.Live2D.Framework.Id;

namespace PersonaEngine.Lib.Live2D.Framework;

/// <summary>
///     Live2D Cubism Original Workflow SDKのエントリポイント
///     利用開始時はCubismFramework.Initialize()を呼び、CubismFramework.Dispose()で終了する。
/// </summary>
public static class CubismFramework
{
    /// <summary>
    ///     メッシュ頂点のオフセット値
    /// </summary>
    public const int VertexOffset = 0;

    /// <summary>
    ///     メッシュ頂点のステップ値
    /// </summary>
    public const int VertexStep = 2;

    private static ICubismAllocator? s_allocator;

    private static Option? s_option;

    /// <summary>
    ///     IDマネージャのインスタンスを取得する。
    /// </summary>
    public static CubismIdManager CubismIdManager { get; private set; } = new();

    public static bool IsInitialized { get; private set; }

    public static bool IsStarted { get; private set; }

    /// <summary>
    ///     Cubism FrameworkのAPIを使用可能にする。
    ///     APIを実行する前に必ずこの関数を実行すること。
    ///     引数に必ずメモリアロケータを渡してください。
    ///     一度準備が完了して以降は、再び実行しても内部処理がスキップされます。
    /// </summary>
    /// <param name="allocator">ICubismAllocatorクラスのインスタンス</param>
    /// <param name="option">Optionクラスのインスタンス</param>
    /// <returns>準備処理が完了したらtrueが返ります。</returns>
    public static bool StartUp(ICubismAllocator allocator, Option option)
    {
        if ( IsStarted )
        {
            CubismLog.Info("[Live2D SDK]CubismFramework.StartUp() is already done.");

            return IsStarted;
        }

        s_option = option;
        if ( s_option != null )
        {
            CubismCore.SetLogFunction(s_option.LogFunction);
        }

        if ( allocator == null )
        {
            CubismLog.Warning("[Live2D SDK]CubismFramework.StartUp() failed, need allocator instance.");
            IsStarted = false;
        }
        else
        {
            s_allocator = allocator;
            IsStarted   = true;
        }

        //Live2D Cubism Coreバージョン情報を表示
        if ( IsStarted )
        {
            var version = CubismCore.GetVersion();

            var major         = (version & 0xFF000000) >> 24;
            var minor         = (version & 0x00FF0000) >> 16;
            var patch         = version & 0x0000FFFF;
            var versionNumber = version;

            CubismLog.Info($"[Live2D SDK]Cubism Core version: {major:#0}.{minor:0}.{patch:0000} ({versionNumber})");
        }

        CubismLog.Info("[Live2D SDK]CubismFramework.StartUp() is complete.");

        return IsStarted;
    }

    /// <summary>
    ///     StartUp()で初期化したCubismFrameworkの各パラメータをクリアします。
    ///     Dispose()したCubismFrameworkを再利用する際に利用してください。
    /// </summary>
    public static void CleanUp()
    {
        IsStarted     = false;
        IsInitialized = false;
    }

    /// <summary>
    ///     Cubism Framework内のリソースを初期化してモデルを表示可能な状態にします。
    ///     再度Initialize()するには先にDispose()を実行する必要があります。
    /// </summary>
    public static void Initialize()
    {
        if ( !IsStarted )
        {
            CubismLog.Warning("[Live2D SDK]CubismFramework is not started.");

            return;
        }

        // --- s_isInitializedによる連続初期化ガード ---
        // 連続してリソース確保が行われないようにする。
        // 再度Initialize()するには先にDispose()を実行する必要がある。
        if ( IsInitialized )
        {
            CubismLog.Warning("[Live2D SDK]CubismFramework.Initialize() skipped, already initialized.");

            return;
        }

        IsInitialized = true;

        CubismLog.Info("[Live2D SDK]CubismFramework.Initialize() is complete.");
    }

    /// <summary>
    ///     Cubism Framework内の全てのリソースを解放します。
    ///     ただし、外部で確保されたリソースについては解放しません。
    ///     外部で適切に破棄する必要があります。
    /// </summary>
    public static void Dispose()
    {
        if ( !IsStarted )
        {
            CubismLog.Warning("[Live2D SDK]CubismFramework is not started.");

            return;
        }

        // --- s_isInitializedによる未初期化解放ガード ---
        // Dispose()するには先にInitialize()を実行する必要がある。
        if ( !IsInitialized ) // false...リソース未確保の場合
        {
            CubismLog.Warning("[Live2D SDK]CubismFramework.Dispose() skipped, not initialized.");

            return;
        }

        IsInitialized = false;

        CubismLog.Info("[Live2D SDK]CubismFramework.Dispose() is complete.");
    }

    /// <summary>
    ///     Core APIにバインドしたログ関数を実行する
    /// </summary>
    /// <param name="data">ログメッセージ</param>
    public static void CoreLogFunction(string data) { CubismCore.GetLogFunction()?.Invoke(data); }

    /// <summary>
    ///     現在のログ出力レベル設定の値を返す。
    /// </summary>
    /// <returns>現在のログ出力レベル設定の値</returns>
    public static LogLevel GetLoggingLevel()
    {
        if ( s_option != null )
        {
            return s_option.LoggingLevel;
        }

        return LogLevel.Off;
    }

    public static IntPtr Allocate(int size) { return s_allocator!.Allocate(size); }

    public static IntPtr AllocateAligned(int size, int alignment) { return s_allocator!.AllocateAligned(size, alignment); }

    public static void Deallocate(IntPtr address) { s_allocator!.Deallocate(address); }

    public static void DeallocateAligned(IntPtr address) { s_allocator!.DeallocateAligned(address); }
}

using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

namespace PersonaEngine.Lib.UI;

/// <summary>
///     Wraps Silk.NET window creation, events, and OpenGL context initialization.
/// </summary>
public class WindowManager
{
    public WindowManager(Vector2D<int> size, string title)
    {
        var options = WindowOptions.Default;
        options.Size             =  size;
        options.Title            =  title;
        options.UpdatesPerSecond =  60;
        options.FramesPerSecond  =  30;
        options.WindowBorder     =  WindowBorder.Fixed;
        MainWindow               =  Window.Create(options);
        MainWindow.Load          += OnLoad;
        MainWindow.Update        += OnUpdate;
        MainWindow.Render        += OnRender;
        MainWindow.Resize        += OnResize;
        MainWindow.Closing       += OnClose;
    }

    public IWindow MainWindow { get; }

    public GL GL { get; private set; }

    public event Action<double> RenderFrame;

    public event Action Load;

    public event Action<Vector2D<int>> Resize;

    public event Action<double> Update;

    public event Action Close;

    private void OnLoad()
    {
        GL = GL.GetApi(MainWindow);
        Load?.Invoke();
    }

    private void OnUpdate(double delta) { Update?.Invoke(delta); }

    private void OnRender(double delta) { RenderFrame?.Invoke(delta); }

    private void OnResize(Vector2D<int> size) { Resize?.Invoke(size); }

    private void OnClose() { Close?.Invoke(); }

    public void Run() { MainWindow.Run(); }
}

using PersonaEngine.Lib.UI.Common;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.Text.Rendering;

public class VertexArrayObject : IDisposable
{
    private readonly GL _gl;

    private readonly uint _handle;

    private readonly int _stride;

    public VertexArrayObject(GL glApi, int stride)
    {
        _gl = glApi;

        if ( stride <= 0 )
        {
            throw new ArgumentOutOfRangeException(nameof(stride));
        }

        _stride = stride;

        _gl.GenVertexArrays(1, out _handle);
        _gl.CheckError();
    }

    public void Dispose()
    {
        _gl.DeleteVertexArray(_handle);
        _gl.CheckError();
    }

    public void Bind()
    {
        _gl.BindVertexArray(_handle);
        _gl.CheckError();
    }

    public unsafe void VertexAttribPointer(int location, int size, VertexAttribPointerType type, bool normalized, int offset)
    {
        _gl.EnableVertexAttribArray((uint)location);
        _gl.VertexAttribPointer((uint)location, size, type, normalized, (uint)_stride, (void*)offset);
        _gl.CheckError();
    }
}

using System.Numerics;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Notification data structure
/// </summary>
public class Notification
{
    public enum NotificationType
    {
        Info,

        Success,

        Warning,

        Error
    }

    public Notification(NotificationType type, string message, float duration = 5f, Action? action = null, string actionLabel = "OK")
    {
        Type          = type;
        Message       = message;
        RemainingTime = duration;
        Action        = action;
        ActionLabel   = actionLabel;
    }

    public string Id { get; } = Guid.NewGuid().ToString();

    public NotificationType Type { get; }

    public string Message { get; }

    public float RemainingTime { get; set; }

    public Action? Action { get; }

    public string ActionLabel { get; }

    public bool HasAction => Action != null;

    public void InvokeAction() { Action?.Invoke(); }

    public Vector4 GetBackgroundColor()
    {
        return Type switch {
            NotificationType.Info => new Vector4(0.2f, 0.4f, 0.8f, 0.2f),
            NotificationType.Success => new Vector4(0.2f, 0.8f, 0.2f, 0.2f),
            NotificationType.Warning => new Vector4(0.9f, 0.7f, 0.0f, 0.2f),
            NotificationType.Error => new Vector4(0.8f, 0.2f, 0.2f, 0.25f),
            _ => new Vector4(0.3f, 0.3f, 0.3f, 0.2f)
        };
    }

    public Vector4 GetTextColor()
    {
        return Type switch {
            NotificationType.Info => new Vector4(0.3f, 0.6f, 1.0f, 1.0f),
            NotificationType.Success => new Vector4(0.0f, 0.8f, 0.0f, 1.0f),
            NotificationType.Warning => new Vector4(1.0f, 0.7f, 0.0f, 1.0f),
            NotificationType.Error => new Vector4(1.0f, 0.3f, 0.3f, 1.0f),
            _ => new Vector4(0.9f, 0.9f, 0.9f, 1.0f)
        };
    }
}

using System.Collections.Immutable;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Implements editor state management
/// </summary>
public class EditorStateManager : IEditorStateManager
{
    private readonly Dictionary<string, ActiveOperation> _activeOperations = new();

    private readonly Dictionary<string, object> _stateValues = new();

    public event EventHandler<EditorStateChangedEventArgs> StateChanged;

    public bool HasUnsavedChanges { get; private set; } = false;

    public void MarkAsChanged(string? sectionKey = null)
    {
        SetStateValue("HasUnsavedChanges", false, true);
        HasUnsavedChanges = true;
    }

    public void MarkAsSaved()
    {
        SetStateValue("HasUnsavedChanges", true, false);
        HasUnsavedChanges = false;
    }

    public ActiveOperation? GetActiveOperation() { return _activeOperations.Values.FirstOrDefault(); }

    public void RegisterActiveOperation(ActiveOperation operation)
    {
        _activeOperations[operation.Id] = operation;
        FireStateChanged("ActiveOperations", null, _activeOperations.Values.ToImmutableList());
    }

    public void ClearActiveOperation(string operationId)
    {
        if ( _activeOperations.TryGetValue(operationId, out var operation) )
        {
            _activeOperations.Remove(operationId);
            operation.CancellationSource.Dispose();
            FireStateChanged("ActiveOperations", null, _activeOperations.Values.ToImmutableList());
        }
    }

    private void SetStateValue<T>(string key, T? oldValue, T newValue) where T : struct
    {
        _stateValues[key] = newValue;
        FireStateChanged(key, oldValue, newValue);
    }

    private void FireStateChanged<T>(string key, T? oldValue, T newValue) where T : struct { StateChanged?.Invoke(this, new EditorStateChangedEventArgs(key, oldValue, newValue)); }

    private void FireStateChanged<T>(string key, T? oldValue, T newValue) { StateChanged?.Invoke(this, new EditorStateChangedEventArgs(key, oldValue, newValue)); }
}

using PersonaEngine.Lib.Configuration;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.Spout;

public class SpoutRegistry : IDisposable
{
    private readonly SpoutConfiguration[] _configs;

    private readonly GL _gl;

    private readonly Dictionary<string, SpoutManager> _spoutManagers = new();

    public SpoutRegistry(GL gl, SpoutConfiguration[] configs)
    {
        _gl      = gl;
        _configs = configs;

        foreach ( var config in _configs )
        {
            GetOrCreateManager(config);
        }
    }

    public void Dispose()
    {
        foreach ( var manager in _spoutManagers.Values )
        {
            manager.Dispose();
        }

        _spoutManagers.Clear();
    }

    public SpoutManager GetOrCreateManager(SpoutConfiguration config)
    {
        if ( !_spoutManagers.TryGetValue(config.OutputName, out var manager) )
        {
            manager = new SpoutManager(_gl, config);
            _spoutManagers.Add(config.OutputName, manager);
        }

        return manager;
    }

    public void BeginFrame(string spoutName)
    {
        if ( string.IsNullOrEmpty(spoutName) || !_spoutManagers.TryGetValue(spoutName, out var manager) )
        {
            return;
        }

        manager.BeginFrame();
    }

    public void SendFrame(string spoutName)
    {
        if ( string.IsNullOrEmpty(spoutName) || !_spoutManagers.TryGetValue(spoutName, out var manager) )
        {
            return;
        }

        manager.SendFrame();
    }

    public void ResizeAll(int width, int height)
    {
        foreach ( var manager in _spoutManagers.Values )
        {
            manager.ResizeFramebuffer(width, height);
        }
    }
}

using System.Text.Json;
using System.Text.Json.Serialization;

namespace PersonaEngine.Lib.TTS.Synthesis;

public class PhonemeEntryConverter : JsonConverter<PhonemeEntry>
{
    public override PhonemeEntry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using var doc = JsonDocument.ParseValue(ref reader);

        return PhonemeEntry.FromJsonElement(doc.RootElement);
    }

    public override void Write(Utf8JsonWriter writer, PhonemeEntry value, JsonSerializerOptions options)
    {
        switch ( value )
        {
            case SimplePhonemeEntry simple:
                writer.WriteStringValue(simple.Phoneme);

                break;
            case ContextualPhonemeEntry contextual:
                writer.WriteStartObject();
                foreach ( var form in contextual.Forms )
                {
                    writer.WriteString(form.Key, form.Value);
                }

                writer.WriteEndObject();

                break;
            default:
                throw new JsonException($"Unsupported PhonemeEntry type: {value.GetType()}");
        }
    }
}

namespace PersonaEngine.Lib.Audio;

internal class AsyncAutoResetEvent
{
    private static readonly Task Completed = Task.CompletedTask;

    private int isSignaled; // 0 for false, 1 for true

    private TaskCompletionSource<bool>? waitTcs;

    public Task WaitAsync()
    {
        if ( Interlocked.CompareExchange(ref isSignaled, 0, 1) == 1 )
        {
            return Completed;
        }

        var tcs    = new TaskCompletionSource<bool>();
        var oldTcs = Interlocked.Exchange(ref waitTcs, tcs);
        oldTcs?.TrySetCanceled();

        return tcs.Task;
    }

    public void Set()
    {
        var toRelease = Interlocked.Exchange(ref waitTcs, null);
        if ( toRelease != null )
        {
            toRelease.SetResult(true);
        }
        else
        {
            Interlocked.Exchange(ref isSignaled, 1);
        }
    }
}

using System.Text.Json;
using System.Text.Json.Serialization;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Custom JSON converter for lexicon entries
/// </summary>
public class LexiconEntryJsonConverter : JsonConverter<LexiconEntry>
{
    public override LexiconEntry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var entry = new LexiconEntry();

        if ( reader.TokenType == JsonTokenType.String )
        {
            entry.Simple = reader.GetString();
        }
        else if ( reader.TokenType == JsonTokenType.StartObject )
        {
            entry.ByTag = JsonSerializer.Deserialize<Dictionary<string, string>>(ref reader, options);
        }
        else
        {
            throw new JsonException($"Unexpected token {reader.TokenType} when deserializing lexicon entry");
        }

        return entry;
    }

    public override void Write(Utf8JsonWriter writer, LexiconEntry value, JsonSerializerOptions options)
    {
        if ( value.IsSimple )
        {
            writer.WriteStringValue(value.Simple);
        }
        else
        {
            JsonSerializer.Serialize(writer, value.ByTag, options);
        }
    }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Model resource wrapper
/// </summary>
public class ModelResource : IDisposable
{
    private byte[]? _modelData;

    public ModelResource(string path) { Path = path; }

    /// <summary>
    ///     Path to the model
    /// </summary>
    public string Path { get; }

    /// <summary>
    ///     Whether the model is loaded
    /// </summary>
    public bool IsLoaded => _modelData != null;

    public void Dispose() { _modelData = null; }

    /// <summary>
    ///     Gets the model data, loading it if necessary
    /// </summary>
    public async Task<byte[]> GetDataAsync()
    {
        if ( _modelData == null )
        {
            _modelData = await File.ReadAllBytesAsync(Path);
        }

        return _modelData;
    }
}

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for audio synthesis from phonemes
/// </summary>
public interface IAudioSynthesizer : IAsyncDisposable
{
    /// <summary>
    ///     Synthesizes audio from phonemes
    /// </summary>
    /// <param name="phonemes">Phoneme string</param>
    /// <param name="voiceId">Voice identifier</param>
    /// <param name="options">Synthesis options</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>Audio data with timing information</returns>
    Task<AudioData> SynthesizeAsync(
        string              phonemes,
        KokoroVoiceOptions? options           = null,
        CancellationToken   cancellationToken = default);
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Input;
using PersonaEngine.Lib.Utils;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Strategies;

public class MinWordsNoSpeakingStrategy : IBargeInStrategy
{
    public bool ShouldAllowBargeIn(BargeInContext context)
    {
        if ( context.CurrentState == ConversationState.Speaking )
        {
            return false;
        }

        switch ( context.InputEvent )
        {
            case SttSegmentRecognizing segmentRecognizing:
            {
                var wordCount = segmentRecognizing.PartialTranscript.GetWordCount();

                return wordCount >= context.ConversationOptions.BargeInMinWords;
            }
            case SttSegmentRecognized segmentRecognized:
            {
                var wordCount = segmentRecognized.FinalTranscript.GetWordCount();

                return wordCount >= context.ConversationOptions.BargeInMinWords;
            }
            default:
                return false;
        }
    }
}

namespace PersonaEngine.Lib.Audio.Player;

/// <summary>
///     Defines methods for transporting audio data over a network.
/// </summary>
public interface IAudioTransport : IAsyncDisposable
{
    /// <summary>
    ///     Initializes the transport.
    /// </summary>
    Task InitializeAsync();

    /// <summary>
    ///     Sends an audio packet over the transport.
    /// </summary>
    /// <param name="audioData">Audio data to send.</param>
    /// <param name="sampleRate">Sample rate of the audio.</param>
    /// <param name="samplesPerChannel">Number of samples per channel.</param>
    /// <param name="channels">Number of audio channels.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    Task SendAudioPacketAsync(
        ReadOnlyMemory<float> audioData,
        int                   sampleRate,
        int                   samplesPerChannel,
        int                   channels,
        CancellationToken     cancellationToken);

    Task FlushAsync(CancellationToken cancellationToken);
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

/// <summary>
///     モーションの管理を行うクラス。
/// </summary>
public class CubismMotionManager : CubismMotionQueueManager
{
    /// <summary>
    ///     現在再生中のモーションの優先度
    /// </summary>
    public MotionPriority CurrentPriority { get; private set; }

    /// <summary>
    ///     再生予定のモーションの優先度。再生中は0になる。モーションファイルを別スレッドで読み込むときの機能。
    /// </summary>
    public MotionPriority ReservePriority { get; set; }

    /// <summary>
    ///     優先度を設定してモーションを開始する。
    /// </summary>
    /// <param name="motion">モーション</param>
    /// <param name="autoDelete">再生が終了したモーションのインスタンスを削除するならtrue</param>
    /// <param name="priority">優先度</param>
    /// <returns>開始したモーションの識別番号を返す。個別のモーションが終了したか否かを判定するIsFinished()の引数で使用する。開始できない時は「-1」</returns>
    public CubismMotionQueueEntry StartMotionPriority(ACubismMotion motion, MotionPriority priority)
    {
        if ( priority == ReservePriority )
        {
            ReservePriority = 0; // 予約を解除
        }

        CurrentPriority = priority; // 再生中モーションの優先度を設定

        return StartMotion(motion);
    }

    /// <summary>
    ///     モーションを更新して、モデルにパラメータ値を反映する。
    /// </summary>
    /// <param name="model">対象のモデル</param>
    /// <param name="deltaTimeSeconds">デルタ時間[秒]</param>
    /// <returns>
    ///     true    更新されている
    ///     false   更新されていない
    /// </returns>
    public bool UpdateMotion(CubismModel model, float deltaTimeSeconds)
    {
        UserTimeSeconds += deltaTimeSeconds;

        var updated = DoUpdateMotion(model, UserTimeSeconds);

        if ( IsFinished() )
        {
            CurrentPriority = 0; // 再生中モーションの優先度を解除
        }

        return updated;
    }

    /// <summary>
    ///     モーションを予約する。
    /// </summary>
    /// <param name="priority">優先度</param>
    /// <returns>
    ///     true    予約できた
    ///     false   予約できなかった
    /// </returns>
    public bool ReserveMotion(MotionPriority priority)
    {
        if ( priority <= ReservePriority || priority <= CurrentPriority )
        {
            return false;
        }

        ReservePriority = priority;

        return true;
    }
}

namespace PersonaEngine.Lib.Live2D.Framework;

public static class CubismModelSettingJson
{
    // JSON keys
    public const string Version = "Version";

    public const string FileReferences = "FileReferences";

    public const string Groups = "Groups";

    public const string Layout = "Layout";

    public const string HitAreas = "HitAreas";

    public const string Moc = "Moc";

    public const string Textures = "Textures";

    public const string Physics = "Physics";

    public const string DisplayInfo = "DisplayInfo";

    public const string Pose = "Pose";

    public const string Expressions = "Expressions";

    public const string Motions = "Motions";

    public const string UserData = "UserData";

    public const string Name = "Name";

    public const string FilePath = "File";

    public const string Id = "Id";

    public const string Ids = "Ids";

    public const string Target = "Target";

    // Motions
    public const string Idle = "Idle";

    public const string TapBody = "TapBody";

    public const string PinchIn = "PinchIn";

    public const string PinchOut = "PinchOut";

    public const string Shake = "Shake";

    public const string FlickHead = "FlickHead";

    public const string Parameter = "Parameter";

    public const string SoundPath = "Sound";

    public const string FadeInTime = "FadeInTime";

    public const string FadeOutTime = "FadeOutTime";

    // Layout
    public const string CenterX = "CenterX";

    public const string CenterY = "CenterY";

    public const string X = "X";

    public const string Y = "Y";

    public const string Width = "Width";

    public const string Height = "Height";

    public const string LipSync = "LipSync";

    public const string EyeBlink = "EyeBlink";

    public const string InitParameter = "init_param";

    public const string InitPartsVisible = "init_parts_visible";

    public const string Val = "val";

    public static bool GetLayoutMap(this ModelSettingObj obj, Dictionary<string, float> outLayoutMap)
    {
        var node = obj.Layout;
        if ( node == null )
        {
            return false;
        }

        var ret = false;
        foreach ( var item in node )
        {
            if ( outLayoutMap.ContainsKey(item.Key) )
            {
                outLayoutMap[item.Key] = item.Value;
            }
            else
            {
                outLayoutMap.Add(item.Key, item.Value);
            }

            ret = true;
        }

        return ret;
    }

    public static bool IsExistEyeBlinkParameters(this ModelSettingObj obj)
    {
        var node = obj.Groups;
        if ( node == null )
        {
            return false;
        }

        foreach ( var item in node )
        {
            if ( item.Name == EyeBlink )
            {
                return true;
            }
        }

        return false;
    }

    public static bool IsExistLipSyncParameters(this ModelSettingObj obj)
    {
        var node = obj.Groups;
        if ( node == null )
        {
            return false;
        }

        foreach ( var item in node )
        {
            if ( item.Name == LipSync )
            {
                return true;
            }
        }

        return false;
    }
}

namespace PersonaEngine.Lib.TTS.RVC;

public interface IRVCVoiceProvider : IAsyncDisposable
{
    /// <summary>
    ///     Gets the fullpath to the voice model
    /// </summary>
    /// <param name="voiceId">Voice identifier</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>Voice model path</returns>
    Task<string> GetVoiceAsync(
        string            voiceId,
        CancellationToken cancellationToken = default);

    /// <summary>
    ///     Gets all available voice IDs
    /// </summary>
    /// <returns>List of voice IDs</returns>
    Task<IReadOnlyList<string>> GetAvailableVoicesAsync(
        CancellationToken cancellationToken = default);
}

using System.Threading.Channels;

using PersonaEngine.Lib.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

namespace PersonaEngine.Lib.TTS.Synthesis;

public interface ITtsEngine : IDisposable
{
    Task<CompletionReason> SynthesizeStreamingAsync(
        ChannelReader<LlmChunkEvent> inputReader,
        ChannelWriter<IOutputEvent>  outputWriter,
        Guid                         turnId,
        Guid                         sessionId,
        KokoroVoiceOptions?          options           = null,
        CancellationToken            cancellationToken = default
    );
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Implements configuration section registry
/// </summary>
public class ConfigSectionRegistry : IConfigSectionRegistry
{
    private readonly Dictionary<string, IConfigSectionEditor> _sections = new();

    public void RegisterSection(IConfigSectionEditor section)
    {
        if ( section == null )
        {
            throw new ArgumentNullException(nameof(section));
        }

        _sections[section.SectionKey] = section;
    }

    public void UnregisterSection(string sectionKey) { _sections.Remove(sectionKey); }

    public IConfigSectionEditor GetSection(string sectionKey) { return _sections.TryGetValue(sectionKey, out var section) ? section : null; }

    public IReadOnlyList<IConfigSectionEditor> GetSections() { return _sections.Values.ToList().AsReadOnly(); }
}

using System.Collections.Frozen;

namespace PersonaEngine.Lib.TTS.Synthesis;

public static class PhonemizerConstants
{
    public static readonly HashSet<char> Diphthongs = new("AIOQWYʤʧ");

    public static readonly HashSet<char> Vowels = new("AIOQWYaiuæɑɒɔəɛɜɪʊʌᵻ");

    public static readonly HashSet<char> Consonants = new("bdfhjklmnpstvwzðŋɡɹɾʃʒʤʧθ");

    public static readonly char PrimaryStress = 'ˈ';

    public static readonly char SecondaryStress = 'ˌ';

    public static readonly char[] Stresses = ['ˌ', 'ˈ'];

    public static readonly HashSet<char> UsTaus = new("AIOWYiuæɑəɛɪɹʊʌ");

    public static readonly HashSet<char> UsVocab = new("AIOWYbdfhijklmnpstuvwzæðŋɑɔəɛɜɡɪɹɾʃʊʌʒʤʧˈˌθᵊᵻ");

    public static readonly HashSet<char> GbVocab = new("AIQWYabdfhijklmnpstuvwzðŋɑɒɔəɛɜɡɪɹʃʊʌʒʤʧˈˌːθᵊ");

    public static readonly IReadOnlyDictionary<string, (string Dollar, string Cent)> Currencies = new Dictionary<string, (string, string)> { ["$"] = ("dollar", "cent"), ["£"] = ("pound", "pence"), ["€"] = ("euro", "cent") }.ToFrozenDictionary();

    public static readonly Dictionary<string, string> AddSymbols = new() { ["."] = "dot", ["/"] = "slash" };

    public static readonly Dictionary<string, string> Symbols = new() { ["%"] = "percent", ["&"] = "and", ["+"] = "plus", ["@"] = "at" };

    public static readonly HashSet<string> Ordinals = ["st", "nd", "rd", "th"];

    public static readonly HashSet<string> PunctTags = [
        ".",
        ",",
        "-LRB-",
        "-RRB-",
        "``",
        "\"\"",
        "''",
        ":",
        "$",
        "#",
        "NFP"
    ];

    public static readonly Dictionary<string, string> PunctTagPhonemes = new() {
                                                                                ["-LRB-"] = "(",
                                                                                ["-RRB-"] = ")",
                                                                                ["``"]    = "\u2014", // em dash
                                                                                ["\"\""]  = "\u201D", // right double quote
                                                                                ["''"]    = "\u201D"  // right double quote
                                                                               };

    public static readonly HashSet<char> SubtokenJunks = new("',-._''/");

    public static readonly HashSet<char> Puncts = new(";:,.!?—…\"\"\"");

    public static readonly HashSet<char> NonQuotePuncts = [];

    static PhonemizerConstants()
    {
        // Initialize NonQuotePuncts
        foreach ( var c in Puncts )
        {
            if ( c != '"' && c != '"' && c != '"' )
            {
                NonQuotePuncts.Add(c);
            }
        }
    }
}

using System.Text;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

using PersonaEngine.Lib;
using PersonaEngine.Lib.Core;

using Serilog;
using Serilog.Events;

namespace PersonaEngine.App;

internal static class Program
{
    private static async Task Main()
    {
        Console.OutputEncoding = Encoding.UTF8;

        var builder = new ConfigurationBuilder()
                      .SetBasePath(Directory.GetCurrentDirectory())
                      .AddJsonFile("appsettings.json", false, true);

        IConfiguration config   = builder.Build();
        var            services = new ServiceCollection();

        services.AddLogging(loggingBuilder =>
                            {
                                CreateLogger();
                                loggingBuilder.AddSerilog();
                            });

        services.AddMetrics();
        services.AddApp(config);

        var serviceProvider = services.BuildServiceProvider();

        var window = serviceProvider.GetRequiredService<AvatarApp>();
        window.Run();

        await serviceProvider.DisposeAsync();
    }

    private static void CreateLogger()
    {
        Log.Logger = new LoggerConfiguration()
                     .MinimumLevel.Warning()
                     .MinimumLevel.Override("PersonaEngine.Lib.Core.Conversation", LogEventLevel.Information)
                     .Enrich.FromLogContext()
                     .Enrich.With<GuidToEmojiEnricher>()
                     .WriteTo.Console(
                                      outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
                                     )
                     .CreateLogger();
    }
}

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

public class CubismMotionQueueEntry
{
    public CubismMotionQueueEntry()
    {
        Available = true;
        StartTime = -1.0f;
        EndTime   = -1.0f;
    }

    /// <summary>
    ///     Motion
    /// </summary>
    public required ACubismMotion Motion { get; set; }

    /// <summary>
    ///     Activation flag
    /// </summary>
    public bool Available { get; set; }

    /// <summary>
    ///     Completion flag
    /// </summary>
    public bool Finished { get; set; }

    /// <summary>
    ///     Start flag (since 0.9.00)
    /// </summary>
    public bool Started { get; set; }

    /// <summary>
    ///     Motion playback start time [seconds]
    /// </summary>
    public float StartTime { get; set; }

    /// <summary>
    ///     Fade-in start time (only the first time for loops) [seconds]
    /// </summary>
    public float FadeInStartTime { get; set; }

    /// <summary>
    ///     Scheduled end time [seconds]
    /// </summary>
    public float EndTime { get; set; }

    /// <summary>
    ///     Time state [seconds]
    /// </summary>
    public float StateTime { get; private set; }

    /// <summary>
    ///     Weight state
    /// </summary>
    public float StateWeight { get; private set; }

    /// <summary>
    ///     Last time checked by the Motion side
    /// </summary>
    public float LastEventCheckSeconds { get; set; }

    public float FadeOutSeconds { get; private set; }

    public bool IsTriggeredFadeOut { get; private set; }

    /// <summary>
    ///     Set the start of fade-out.
    /// </summary>
    /// <param name="fadeOutSeconds">Time required for fade-out [seconds]</param>
    public void SetFadeout(float fadeOutSeconds)
    {
        FadeOutSeconds     = fadeOutSeconds;
        IsTriggeredFadeOut = true;
    }

    /// <summary>
    ///     Start fade-out.
    /// </summary>
    /// <param name="fadeOutSeconds">Time required for fade-out [seconds]</param>
    /// <param name="userTimeSeconds">Accumulated delta time [seconds]</param>
    public void StartFadeout(float fadeOutSeconds, float userTimeSeconds)
    {
        var newEndTimeSeconds = userTimeSeconds + fadeOutSeconds;
        IsTriggeredFadeOut = true;

        if ( EndTime < 0.0f || newEndTimeSeconds < EndTime )
        {
            EndTime = newEndTimeSeconds;
        }
    }

    /// <summary>
    ///     Set the motion state.
    /// </summary>
    /// <param name="timeSeconds">Current time [seconds]</param>
    /// <param name="weight">Motion weight</param>
    public void SetState(float timeSeconds, float weight)
    {
        StateTime   = timeSeconds;
        StateWeight = weight;
    }
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Input;
using PersonaEngine.Lib.Utils;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Strategies;

public class MinWordsBargeInStrategy : IBargeInStrategy
{
    public bool ShouldAllowBargeIn(BargeInContext context)
    {
        switch ( context.InputEvent )
        {
            case SttSegmentRecognizing segmentRecognizing:
            {
                var wordCount = segmentRecognizing.PartialTranscript.GetWordCount();

                return wordCount >= context.ConversationOptions.BargeInMinWords;
            }
            case SttSegmentRecognized segmentRecognized:
            {
                var wordCount = segmentRecognized.FinalTranscript.GetWordCount();

                return wordCount >= context.ConversationOptions.BargeInMinWords;
            }
            default:
                return false;
        }
    }
}

namespace PersonaEngine.Lib.Audio;

public interface IAudioSource : IDisposable
{
    IReadOnlyDictionary<string, string> Metadata { get; }

    /// <summary>
    ///     Gets the duration of the audio stream.
    /// </summary>
    TimeSpan Duration { get; }

    /// <summary>
    ///     Gets the total duration of the audio stream.
    /// </summary>
    /// <remarks>
    ///     TotalDuration can include virtual frames that are not available in the source.
    /// </remarks>
    TimeSpan TotalDuration { get; }

    /// <summary>
    ///     Gets or sets the sample rate of the audio stream.
    /// </summary>
    uint SampleRate { get; }

    /// <summary>
    ///     Gets the number of frames available in the stream
    /// </summary>
    /// <remarks>
    ///     The total number of samples in the stream is equal to the number of frames multiplied by the number of channels.
    /// </remarks>
    long FramesCount { get; }

    /// <summary>
    ///     Gets the actual number of channels in the source.
    /// </summary>
    /// <remarks>
    ///     Note, that the actual number of channels may be different from the number of channels in the header if the source
    ///     uses an aggregation strategy.
    /// </remarks>
    ushort ChannelCount { get; }

    /// <summary>
    ///     Gets a value indicating whether the stream is initialized.
    /// </summary>
    bool IsInitialized { get; }

    ushort BitsPerSample { get; }

    /// <summary>
    ///     Gets the memory slices for all the samples interleaved by channel.
    /// </summary>
    /// <param name="startFrame">The frame index of the samples to get.</param>
    /// <param name="maxFrames">Optional. The maximum length of the frames to get.</param>
    /// <param name="cancellationToken">The cancellation token to observe.</param>
    /// <returns></returns>
    /// <remarks>
    ///     The maximum length of the returned memory is <seealso cref="ChannelCount" /> * <paramref name="maxFrames" />.
    /// </remarks>
    Task<Memory<float>> GetSamplesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default);

    /// <summary>
    ///     Gets the memory slices for all the frames interleaved by channel.
    /// </summary>
    /// <param name="startFrame">The frame index of the samples to get.</param>
    /// <param name="maxFrames">Optional. The maximum length of the frames to get.</param>
    /// <param name="cancellationToken">The cancellation token to observe.</param>
    /// <returns></returns>
    /// <remarks>
    ///     The maximum length of the returned memory is <seealso cref="ChannelCount" /> * <paramref name="maxFrames" /> *
    ///     <seealso cref="BitsPerSample" /> / 8.
    /// </remarks>
    Task<Memory<byte>> GetFramesAsync(long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default);

    Task<int> CopyFramesAsync(Memory<byte> destination, long startFrame, int maxFrames = int.MaxValue, CancellationToken cancellationToken = default);
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

internal class CubismRendererProfile_OpenGLES2(OpenGLApi gl)
{
    /// <summary>
    ///     モデル描画直前のアクティブなテクスチャ
    /// </summary>
    internal int LastActiveTexture;

    /// <summary>
    ///     モデル描画直前の頂点バッファ
    /// </summary>
    internal int LastArrayBufferBinding;

    /// <summary>
    ///     モデル描画直前のGL_SCISSOR_TESTパラメータ
    /// </summary>
    internal bool LastBlend;

    /// <summary>
    ///     モデル描画直前のカラーブレンディングパラメータ
    /// </summary>
    internal int[] LastBlending = new int[4];

    /// <summary>
    ///     モデル描画直前のGL_COLOR_WRITEMASKパラメータ
    /// </summary>
    internal bool[] LastColorMask = new bool[4];

    /// <summary>
    ///     モデル描画直前のGL_CULL_FACEパラメータ
    /// </summary>
    internal bool LastCullFace;

    /// <summary>
    ///     モデル描画直前のGL_DEPTH_TESTパラメータ
    /// </summary>
    internal bool LastDepthTest;

    /// <summary>
    ///     モデル描画直前のElementバッファ
    /// </summary>
    internal int LastElementArrayBufferBinding;

    /// <summary>
    ///     モデル描画直前のフレームバッファ
    /// </summary>
    internal int LastFBO;

    /// <summary>
    ///     モデル描画直前のGL_CULL_FACEパラメータ
    /// </summary>
    internal int LastFrontFace;

    /// <summary>
    ///     モデル描画直前のシェーダプログラムバッファ
    /// </summary>
    internal int LastProgram;

    /// <summary>
    ///     モデル描画直前のGL_VERTEX_ATTRIB_ARRAY_ENABLEDパラメータ
    /// </summary>
    internal bool LastScissorTest;

    /// <summary>
    ///     モデル描画直前のGL_STENCIL_TESTパラメータ
    /// </summary>
    internal bool LastStencilTest;

    /// <summary>
    ///     モデル描画直前のテクスチャユニット0
    /// </summary>
    internal int LastTexture0Binding2D;

    /// <summary>
    ///     モデル描画直前のテクスチャユニット1
    /// </summary>
    internal int LastTexture1Binding2D;

    /// <summary>
    ///     モデル描画直前のテクスチャユニット1
    /// </summary>
    internal int[] LastVertexAttribArrayEnabled = new int[4];

    /// <summary>
    ///     モデル描画直前のビューポート
    /// </summary>
    internal int[] LastViewport = new int[4];

    /// <summary>
    ///     OpenGLES2のステートを保持する
    /// </summary>
    internal void Save()
    {
        //-- push state --
        gl.GetIntegerv(gl.GL_ARRAY_BUFFER_BINDING, out LastArrayBufferBinding);
        gl.GetIntegerv(gl.GL_ELEMENT_ARRAY_BUFFER_BINDING, out LastElementArrayBufferBinding);
        gl.GetIntegerv(gl.GL_CURRENT_PROGRAM, out LastProgram);

        gl.GetIntegerv(gl.GL_ACTIVE_TEXTURE, out LastActiveTexture);
        gl.ActiveTexture(gl.GL_TEXTURE1); //テクスチャユニット1をアクティブに（以後の設定対象とする）
        gl.GetIntegerv(gl.GL_TEXTURE_BINDING_2D, out LastTexture1Binding2D);

        gl.ActiveTexture(gl.GL_TEXTURE0); //テクスチャユニット0をアクティブに（以後の設定対象とする）
        gl.GetIntegerv(gl.GL_TEXTURE_BINDING_2D, out LastTexture0Binding2D);

        gl.GetVertexAttribiv(0, gl.GL_VERTEX_ATTRIB_ARRAY_ENABLED, out LastVertexAttribArrayEnabled[0]);
        gl.GetVertexAttribiv(1, gl.GL_VERTEX_ATTRIB_ARRAY_ENABLED, out LastVertexAttribArrayEnabled[1]);
        gl.GetVertexAttribiv(2, gl.GL_VERTEX_ATTRIB_ARRAY_ENABLED, out LastVertexAttribArrayEnabled[2]);
        gl.GetVertexAttribiv(3, gl.GL_VERTEX_ATTRIB_ARRAY_ENABLED, out LastVertexAttribArrayEnabled[3]);

        LastScissorTest = gl.IsEnabled(gl.GL_SCISSOR_TEST);
        LastStencilTest = gl.IsEnabled(gl.GL_STENCIL_TEST);
        LastDepthTest   = gl.IsEnabled(gl.GL_DEPTH_TEST);
        LastCullFace    = gl.IsEnabled(gl.GL_CULL_FACE);
        LastBlend       = gl.IsEnabled(gl.GL_BLEND);

        gl.GetIntegerv(gl.GL_FRONT_FACE, out LastFrontFace);

        gl.GetBooleanv(gl.GL_COLOR_WRITEMASK, LastColorMask);

        // backup blending
        gl.GetIntegerv(gl.GL_BLEND_SRC_RGB, out LastBlending[0]);
        gl.GetIntegerv(gl.GL_BLEND_DST_RGB, out LastBlending[1]);
        gl.GetIntegerv(gl.GL_BLEND_SRC_ALPHA, out LastBlending[2]);
        gl.GetIntegerv(gl.GL_BLEND_DST_ALPHA, out LastBlending[3]);

        // モデル描画直前のFBOとビューポートを保存
        gl.GetIntegerv(gl.GL_FRAMEBUFFER_BINDING, out LastFBO);
        gl.GetIntegerv(gl.GL_VIEWPORT, LastViewport);
    }

    /// <summary>
    ///     保持したOpenGLES2のステートを復帰させる
    /// </summary>
    internal void Restore()
    {
        gl.UseProgram(LastProgram);

        SetGlEnableVertexAttribArray(0, LastVertexAttribArrayEnabled[0] > 0);
        SetGlEnableVertexAttribArray(1, LastVertexAttribArrayEnabled[1] > 0);
        SetGlEnableVertexAttribArray(2, LastVertexAttribArrayEnabled[2] > 0);
        SetGlEnableVertexAttribArray(3, LastVertexAttribArrayEnabled[3] > 0);

        SetGlEnable(gl.GL_SCISSOR_TEST, LastScissorTest);
        SetGlEnable(gl.GL_STENCIL_TEST, LastStencilTest);
        SetGlEnable(gl.GL_DEPTH_TEST, LastDepthTest);
        SetGlEnable(gl.GL_CULL_FACE, LastCullFace);
        SetGlEnable(gl.GL_BLEND, LastBlend);

        gl.FrontFace(LastFrontFace);

        gl.ColorMask(LastColorMask[0], LastColorMask[1], LastColorMask[2], LastColorMask[3]);

        gl.BindBuffer(gl.GL_ARRAY_BUFFER, LastArrayBufferBinding); //前にバッファがバインドされていたら破棄する必要がある
        gl.BindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, LastElementArrayBufferBinding);

        gl.ActiveTexture(gl.GL_TEXTURE1); //テクスチャユニット1を復元
        gl.BindTexture(gl.GL_TEXTURE_2D, LastTexture1Binding2D);

        gl.ActiveTexture(gl.GL_TEXTURE0); //テクスチャユニット0を復元
        gl.BindTexture(gl.GL_TEXTURE_2D, LastTexture0Binding2D);

        gl.ActiveTexture(LastActiveTexture);

        // restore blending
        gl.BlendFuncSeparate(LastBlending[0], LastBlending[1], LastBlending[2], LastBlending[3]);
    }

    /// <summary>
    ///     OpenGLES2の機能の有効・無効をセットする
    /// </summary>
    /// <param name="index">有効・無効にする機能</param>
    /// <param name="enabled">trueなら有効にする</param>
    internal void SetGlEnable(int index, bool enabled)
    {
        if ( enabled )
        {
            gl.Enable(index);
        }
        else
        {
            gl.Disable(index);
        }
    }

    /// <summary>
    ///     OpenGLES2のVertex Attribute Array機能の有効・無効をセットする
    /// </summary>
    /// <param name="index">有効・無効にする機能</param>
    /// <param name="enabled">trueなら有効にする</param>
    internal void SetGlEnableVertexAttribArray(int index, bool enabled)
    {
        if ( enabled )
        {
            gl.EnableVertexAttribArray(index);
        }
        else
        {
            gl.DisableVertexAttribArray(index);
        }
    }
}

using System.Collections.Concurrent;
using System.ComponentModel;
using System.Reflection;

namespace PersonaEngine.Lib.Utils;

public static class EnumExtensions
{
    // Thread-safe cache for storing descriptions
    private static readonly ConcurrentDictionary<(Type Type, string Name), string> DescriptionCache = new();

    /// <summary>
    ///     Gets the description of an enum value.
    ///     If the enum value has a <see cref="DescriptionAttribute" />, returns its value.
    ///     Otherwise, returns the string representation of the enum value.
    /// </summary>
    /// <param name="value">The enum value.</param>
    /// <returns>The description or the string representation if no description is found.</returns>
    public static string GetDescription(this Enum value)
    {
        var type = value.GetType();
        var name = Enum.GetName(type, value);

        if ( name == null )
        {
            return value.ToString();
        }

        var cacheKey = (type, name);

        return DescriptionCache.GetOrAdd(cacheKey, key =>
                                                   {
                                                       var field = key.Type.GetField(key.Name);

                                                       if ( field == null )
                                                       {
                                                           return key.Name;
                                                       }

                                                       var attribute = field.GetCustomAttribute<DescriptionAttribute>(false);

                                                       return attribute?.Description ?? key.Name;
                                                   });
    }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

public class TokenContext
{
    public bool? FutureVowel { get; set; }

    public bool FutureTo { get; set; }

    public static TokenContext UpdateContext(TokenContext ctx, string? phonemes, Token token)
    {
        var vowel = ctx.FutureVowel;

        if ( !string.IsNullOrEmpty(phonemes) )
        {
            foreach ( var c in phonemes )
            {
                if ( PhonemizerConstants.Vowels.Contains(c) ||
                     PhonemizerConstants.Consonants.Contains(c) ||
                     PhonemizerConstants.NonQuotePuncts.Contains(c) )
                {
                    vowel = PhonemizerConstants.NonQuotePuncts.Contains(c) ? null : PhonemizerConstants.Vowels.Contains(c);

                    break;
                }
            }
        }

        return new TokenContext { FutureVowel = vowel, FutureTo = token.IsTo() };
    }
}

using System.ComponentModel;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Types of models
/// </summary>
public enum ModelType
{
    [Description("kokoro/model_slim.onnx")]
    KokoroSynthesis,

    [Description("kokoro/voices")] KokoroVoices,

    [Description("kokoro/phoneme_to_id.txt")]
    KokoroPhonemeMappings,

    [Description("opennlp")] OpenNLPDir,

    [Description("rvc/voices")] RVCVoices,

    [Description("rvc/vec-768-layer-12.onnx")]
    RVCHubert,

    [Description("rvc/crepe_tiny.onnx")] RVCCrepeTiny
}

using System.Text.Json;

namespace PersonaEngine.Lib.TTS.Synthesis;

public abstract record PhonemeEntry
{
    // Factory method to create the appropriate entry type from JSON
    public static PhonemeEntry FromJsonElement(JsonElement element)
    {
        if ( element.ValueKind == JsonValueKind.String )
        {
            return new SimplePhonemeEntry(element.GetString()!);
        }

        if ( element.ValueKind == JsonValueKind.Object )
        {
            var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            foreach ( var property in element.EnumerateObject() )
            {
                if ( property.Value.ValueKind == JsonValueKind.String )
                {
                    dict[property.Name] = property.Value.GetString()!;
                }
            }

            return new ContextualPhonemeEntry(dict);
        }

        throw new JsonException($"Unexpected JSON element type: {element.ValueKind}");
    }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

public record ContextualPhonemeEntry(IReadOnlyDictionary<string, string> Forms) : PhonemeEntry
{
    public string? GetForm(string? tag, TokenContext? ctx)
    {
        // First try exact tag match
        if ( tag != null && Forms.TryGetValue(tag, out var exactMatch) )
        {
            return exactMatch;
        }

        // Try context-specific form
        if ( ctx?.FutureVowel == null && Forms.TryGetValue("None", out var noneForm) )
        {
            return noneForm;
        }

        // Try parent tag
        var parentTag = LexiconUtils.GetParentTag(tag);
        if ( parentTag != null && Forms.TryGetValue(parentTag, out var parentMatch) )
        {
            return parentMatch;
        }

        // Finally, use default
        return Forms.TryGetValue("DEFAULT", out var defaultForm) ? defaultForm : null;
    }
}

namespace PersonaEngine.Lib.Configuration;

public record RVCFilterOptions
{
    public string DefaultVoice { get; init; } = "KasumiVA";

    public bool Enabled { get; set; } = true;

    public int HopSize { get; set; } = 64;

    public int SpeakerId { get; set; } = 0;

    public int F0UpKey { get; set; } = 0;
}

using System.Runtime.InteropServices;

using PersonaEngine.Lib.Live2D.Framework;

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     メモリ確保・解放処理のインターフェースの実装。
///     フレームワークから呼び出される。
/// </summary>
public class LAppAllocator : ICubismAllocator
{
    /// <summary>
    ///     メモリ領域を割り当てる。
    /// </summary>
    /// <param name="size">割り当てたいサイズ。</param>
    /// <returns>指定したメモリ領域</returns>
    public IntPtr Allocate(int size) { return Marshal.AllocHGlobal(size); }

    /// <summary>
    ///     メモリ領域を解放する
    /// </summary>
    /// <param name="memory">解放するメモリ。</param>
    public void Deallocate(IntPtr memory) { Marshal.FreeHGlobal(memory); }

    /// <summary>
    /// </summary>
    /// <param name="size">割り当てたいサイズ。</param>
    /// <param name="alignment">割り当てたいサイズ。</param>
    /// <returns>alignedAddress</returns>
    public unsafe IntPtr AllocateAligned(int size, int alignment)
    {
        IntPtr offset,
               shift,
               alignedAddress;

        IntPtr allocation;
        void** preamble;

        offset = alignment - 1 + sizeof(void*);

        allocation = Allocate((int)(size + offset));

        alignedAddress = allocation + sizeof(void*);

        shift = alignedAddress % alignment;

        if ( shift != 0 )
        {
            alignedAddress += alignment - shift;
        }

        preamble     = (void**)alignedAddress;
        preamble[-1] = (void*)allocation;

        return alignedAddress;
    }

    /// <summary>
    /// </summary>
    /// <param name="alignedMemory">解放するメモリ。</param>
    public unsafe void DeallocateAligned(IntPtr alignedMemory)
    {
        var preamble = (void**)alignedMemory;

        Deallocate(new IntPtr(preamble[-1]));
    }
}

using System.Threading.Channels;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;

public interface IOutputAdapter : IAsyncDisposable
{
    Guid AdapterId { get; }

    ValueTask InitializeAsync(Guid sessionId, CancellationToken cancellationToken);

    ValueTask StartAsync(CancellationToken cancellationToken);

    ValueTask StopAsync(CancellationToken cancellationToken);
}

public interface IAudioOutputAdapter : IOutputAdapter
{
    Task SendAsync(
        ChannelReader<TtsChunkEvent> inputReader,
        ChannelWriter<IOutputEvent>  outputWriter,
        Guid                         turnId,
        CancellationToken            cancellationToken = default);
}

public interface ITextOutputAdapter : IOutputAdapter
{
    Task SendAsync(
        ChannelReader<IOutputEvent> inputReader,
        ChannelWriter<IOutputEvent> outputWriter,
        Guid                        turnId,
        CancellationToken           cancellationToken = default);
}

using System.Numerics;
using System.Runtime.InteropServices;

namespace PersonaEngine.Lib.Live2D.Framework.Core;

public static class CsmEnum
{
    //Alignment constraints.

    /// <summary>
    ///     Necessary alignment for mocs (in bytes).
    /// </summary>
    public const int csmAlignofMoc = 64;

    /// <summary>
    ///     Necessary alignment for models (in bytes).
    /// </summary>
    public const int CsmAlignofModel = 16;

    //Bit masks for non-dynamic drawable flags.

    /// <summary>
    ///     Additive blend mode mask.
    /// </summary>
    public const byte CsmBlendAdditive = 1 << 0;

    /// <summary>
    ///     Multiplicative blend mode mask.
    /// </summary>
    public const byte CsmBlendMultiplicative = 1 << 1;

    /// <summary>
    ///     Double-sidedness mask.
    /// </summary>
    public const byte CsmIsDoubleSided = 1 << 2;

    /// <summary>
    ///     Clipping mask inversion mode mask.
    /// </summary>
    public const byte CsmIsInvertedMask = 1 << 3;

    //Bit masks for dynamic drawable flags.

    /// <summary>
    ///     Flag set when visible.
    /// </summary>
    public const byte CsmIsVisible = 1 << 0;

    /// <summary>
    ///     Flag set when visibility did change.
    /// </summary>
    public const byte CsmVisibilityDidChange = 1 << 1;

    /// <summary>
    ///     Flag set when opacity did change.
    /// </summary>
    public const byte CsmOpacityDidChange = 1 << 2;

    /// <summary>
    ///     Flag set when draw order did change.
    /// </summary>
    public const byte CsmDrawOrderDidChange = 1 << 3;

    /// <summary>
    ///     Flag set when render order did change.
    /// </summary>
    public const byte CsmRenderOrderDidChange = 1 << 4;

    /// <summary>
    ///     Flag set when vertex positions did change.
    /// </summary>
    public const byte CsmVertexPositionsDidChange = 1 << 5;

    /// <summary>
    ///     Flag set when blend color did change.
    /// </summary>
    public const byte CsmBlendColorDidChange = 1 << 6;

    //moc3 file format version.

    /// <summary>
    ///     unknown
    /// </summary>
    public const int CsmMocVersion_Unknown = 0;

    /// <summary>
    ///     moc3 file version 3.0.00 - 3.2.07
    /// </summary>
    public const int CsmMocVersion_30 = 1;

    /// <summary>
    ///     moc3 file version 3.3.00 - 3.3.03
    /// </summary>
    public const int CsmMocVersion_33 = 2;

    /// <summary>
    ///     moc3 file version 4.0.00 - 4.1.05
    /// </summary>
    public const int CsmMocVersion_40 = 3;

    /// <summary>
    ///     moc3 file version 4.2.00 -
    /// </summary>
    public const int CsmMocVersion_42 = 4;

    //Parameter types.

    /// <summary>
    ///     Normal parameter.
    /// </summary>
    public const int CsmParameterType_Normal = 0;

    /// <summary>
    ///     Parameter for blend shape.
    /// </summary>
    public const int CsmParameterType_BlendShape = 1;
}

/// <summary>
///     Log handler.
/// </summary>
/// <param name="message">Null-terminated string message to log.</param>
public delegate void LogFunction(string message);

public static partial class CubismCore
{
    //VERSION

    public static uint Version() { return GetVersion(); }

    /// <summary>
    ///     Queries Core version.
    /// </summary>
    /// <returns>Core version.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetVersion")]
    internal static partial uint GetVersion();

    /// <summary>
    ///     Gets Moc file supported latest version.
    /// </summary>
    /// <returns>csmMocVersion (Moc file latest format version).</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetLatestMocVersion")]
    internal static partial uint GetLatestMocVersion();

    /// <summary>
    ///     Gets Moc file format version.
    /// </summary>
    /// <param name="address">Address of moc.</param>
    /// <param name="size">Size of moc (in bytes).</param>
    /// <returns>csmMocVersion</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetMocVersion")]
    internal static partial uint GetMocVersion(IntPtr address, int size);

    //CONSISTENCY

    /// <summary>
    ///     Checks consistency of a moc.
    /// </summary>
    /// <param name="address">Address of unrevived moc. The address must be aligned to 'csmAlignofMoc'.</param>
    /// <param name="size">Size of moc (in bytes).</param>
    /// <returns>'1' if Moc is valid; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmHasMocConsistency")]
    [return: MarshalAs(UnmanagedType.I4)]
    internal static partial bool HasMocConsistency(IntPtr address, int size);

    //LOGGING

    /// <summary>
    ///     Queries log handler.
    /// </summary>
    /// <returns>Log handler.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetLogFunction")]
    internal static partial LogFunction GetLogFunction();

    /// <summary>
    ///     Sets log handler.
    /// </summary>
    /// <param name="handler">Handler to use.</param>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmSetLogFunction")]
    internal static partial void SetLogFunction(LogFunction handler);

    //MOC

    /// <summary>
    ///     Tries to revive a moc from bytes in place.
    /// </summary>
    /// <param name="address">Address of unrevived moc. The address must be aligned to 'csmAlignofMoc'.</param>
    /// <param name="size">Size of moc (in bytes).</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmReviveMocInPlace")]
    internal static partial IntPtr ReviveMocInPlace(IntPtr address, int size);

    //MODEL

    /// <summary>
    ///     Queries size of a model in bytes.
    /// </summary>
    /// <param name="moc">Moc to query.</param>
    /// <returns>Valid size on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetSizeofModel")]
    internal static partial int GetSizeofModel(IntPtr moc);

    /// <summary>
    ///     Tries to instantiate a model in place.
    /// </summary>
    /// <param name="moc">Source moc.</param>
    /// <param name="address">Address to place instance at. Address must be aligned to 'csmAlignofModel'.</param>
    /// <param name="size">Size of memory block for instance (in bytes).</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmInitializeModelInPlace")]
    internal static partial IntPtr InitializeModelInPlace(IntPtr moc, IntPtr address, int size);

    /// <summary>
    ///     Updates a model.
    /// </summary>
    /// <param name="model">Model to update.</param>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmUpdateModel")]
    internal static partial void UpdateModel(IntPtr model);

    //CANVAS

    /// <summary>
    ///     Reads info on a model canvas.
    /// </summary>
    /// <param name="model">Model query.</param>
    /// <param name="outSizeInPixels">Canvas dimensions.</param>
    /// <param name="outOriginInPixels">Origin of model on canvas.</param>
    /// <param name="outPixelsPerUnit">Aspect used for scaling pixels to units.</param>
    [DllImport("Live2DCubismCore", EntryPoint = "csmReadCanvasInfo")]
#pragma warning disable SYSLIB1054 // 使用 “LibraryImportAttribute” 而不是 “DllImportAttribute” 在编译时生成 P/Invoke 封送代码
    internal static extern void ReadCanvasInfo(IntPtr model, out Vector2 outSizeInPixels,
#pragma warning restore SYSLIB1054 // 使用 “LibraryImportAttribute” 而不是 “DllImportAttribute” 在编译时生成 P/Invoke 封送代码
                                               out Vector2 outOriginInPixels, out float outPixelsPerUnit);

    //PARAMETERS

    /// <summary>
    ///     Gets number of parameters.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid count on success; '-1' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterCount")]
    internal static partial int GetParameterCount(IntPtr model);

    /// <summary>
    ///     Gets parameter IDs.
    ///     All IDs are null-terminated ANSI strings.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterIds")]
    internal static unsafe partial sbyte** GetParameterIds(IntPtr model);

    /// <summary>
    ///     Gets parameter types.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterTypes")]
    internal static unsafe partial int* GetParameterTypes(IntPtr model);

    /// <summary>
    ///     Gets minimum parameter values.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterMinimumValues")]
    internal static unsafe partial float* GetParameterMinimumValues(IntPtr model);

    /// <summary>
    ///     Gets maximum parameter values.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterMaximumValues")]
    internal static unsafe partial float* GetParameterMaximumValues(IntPtr model);

    /// <summary>
    ///     Gets default parameter values.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterDefaultValues")]
    internal static unsafe partial float* GetParameterDefaultValues(IntPtr model);

    /// <summary>
    ///     Gets read/write parameter values buffer.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterValues")]
    internal static unsafe partial float* GetParameterValues(IntPtr model);

    /// <summary>
    ///     Gets number of key values of each parameter.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterKeyCounts")]
    internal static unsafe partial int* GetParameterKeyCounts(IntPtr model);

    /// <summary>
    ///     Gets key values of each parameter.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetParameterKeyValues")]
    internal static unsafe partial float** GetParameterKeyValues(IntPtr model);

    //PARTS

    /// <summary>
    ///     Gets number of parts.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid count on success; '-1' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetPartCount")]
    internal static partial int GetPartCount(IntPtr model);

    /// <summary>
    ///     Gets parts IDs.
    ///     All IDs are null-terminated ANSI strings.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetPartIds")]
    internal static unsafe partial sbyte** GetPartIds(IntPtr model);

    /// <summary>
    ///     Gets read/write part opacities buffer.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetPartOpacities")]
    internal static unsafe partial float* GetPartOpacities(IntPtr model);

    /// <summary>
    ///     Gets part's parent part indices.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetPartParentPartIndices")]
    internal static unsafe partial int* GetPartParentPartIndices(IntPtr model);

    //DRAWABLES

    /// <summary>
    ///     Gets number of drawables.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid count on success; '-1' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableCount")]
    internal static partial int GetDrawableCount(IntPtr model);

    /// <summary>
    ///     Gets drawable IDs.
    ///     All IDs are null-terminated ANSI strings.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableIds")]
    internal static unsafe partial sbyte** GetDrawableIds(IntPtr model);

    /// <summary>
    ///     Gets constant drawable flags.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableConstantFlags")]
    internal static unsafe partial byte* GetDrawableConstantFlags(IntPtr model);

    /// <summary>
    ///     Gets dynamic drawable flags.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableDynamicFlags")]
    internal static unsafe partial byte* GetDrawableDynamicFlags(IntPtr model);

    /// <summary>
    ///     Gets drawable texture indices.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableTextureIndices")]
    internal static unsafe partial int* GetDrawableTextureIndices(IntPtr model);

    /// <summary>
    ///     Gets drawable draw orders.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableDrawOrders")]
    internal static unsafe partial int* GetDrawableDrawOrders(IntPtr model);

    /// <summary>
    ///     Gets drawable render orders.
    ///     The higher the order, the more up front a drawable is.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0'otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableRenderOrders")]
    internal static unsafe partial int* GetDrawableRenderOrders(IntPtr model);

    /// <summary>
    ///     Gets drawable opacities.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableOpacities")]
    internal static unsafe partial float* GetDrawableOpacities(IntPtr model);

    /// <summary>
    ///     Gets numbers of masks of each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableMaskCounts")]
    internal static unsafe partial int* GetDrawableMaskCounts(IntPtr model);

    /// <summary>
    ///     Gets number of vertices of each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableMasks")]
    internal static unsafe partial int** GetDrawableMasks(IntPtr model);

    /// <summary>
    ///     Gets number of vertices of each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableVertexCounts")]
    internal static unsafe partial int* GetDrawableVertexCounts(IntPtr model);

    /// <summary>
    ///     Gets vertex position data of each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; a null pointer otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableVertexPositions")]
    internal static unsafe partial Vector2** GetDrawableVertexPositions(IntPtr model);

    /// <summary>
    ///     Gets texture coordinate data of each drawables.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableVertexUvs")]
    internal static unsafe partial Vector2** GetDrawableVertexUvs(IntPtr model);

    /// <summary>
    ///     Gets number of triangle indices for each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableIndexCounts")]
    internal static unsafe partial int* GetDrawableIndexCounts(IntPtr model);

    /// <summary>
    ///     Gets triangle index data for each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableIndices")]
    internal static unsafe partial ushort** GetDrawableIndices(IntPtr model);

    /// <summary>
    ///     Gets multiply color data for each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableMultiplyColors")]
    internal static unsafe partial Vector4* GetDrawableMultiplyColors(IntPtr model);

    /// <summary>
    ///     Gets screen color data for each drawable.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableScreenColors")]
    internal static unsafe partial Vector4* GetDrawableScreenColors(IntPtr model);

    /// <summary>
    ///     Gets drawable's parent part indices.
    /// </summary>
    /// <param name="model">Model to query.</param>
    /// <returns>Valid pointer on success; '0' otherwise.</returns>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmGetDrawableParentPartIndices")]
    internal static unsafe partial int* GetDrawableParentPartIndices(IntPtr model);

    /// <summary>
    ///     Resets all dynamic drawable flags.
    /// </summary>
    /// <param name="model">Model containing flags.</param>
    [LibraryImport("Live2DCubismCore", EntryPoint = "csmResetDrawableDynamicFlags")]
    internal static unsafe partial void ResetDrawableDynamicFlags(IntPtr model);
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering;

/// <summary>
///     テクスチャの色をRGBAで扱うための構造体
/// </summary>
public struct CubismTextureColor
{
    /// <summary>
    ///     赤チャンネル
    /// </summary>
    public float R;

    /// <summary>
    ///     緑チャンネル
    /// </summary>
    public float G;

    /// <summary>
    ///     青チャンネル
    /// </summary>
    public float B;

    /// <summary>
    ///     αチャンネル
    /// </summary>
    public float A;

    public CubismTextureColor()
    {
        R = 1.0f;
        G = 1.0f;
        B = 1.0f;
        A = 1.0f;
    }

    public CubismTextureColor(CubismTextureColor old)
    {
        R = old.R;
        G = old.G;
        B = old.B;
        A = old.A;
        Check();
    }

    public CubismTextureColor(float r, float g, float b, float a)
    {
        R = r;
        G = g;
        B = b;
        A = a;
        Check();
    }

    private void Check()
    {
        R = R > 1.0f ? 1f : R;
        G = G > 1.0f ? 1f : G;
        B = B > 1.0f ? 1f : B;
        A = A > 1.0f ? 1f : A;
    }
}

using PersonaEngine.Lib.UI.Common;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.GUI;

public class ConfigSectionRegistrationTask : IStartupTask
{
    private readonly ChatEditor _chatEditor;

    private readonly IConfigSectionRegistry _registry;

    private readonly RouletteWheelEditor _rouletteWheelEditor;

    private readonly TtsConfigEditor _ttsEditor;

    private readonly MicrophoneConfigEditor _microphoneConfigEditor;

    public ConfigSectionRegistrationTask(IConfigSectionRegistry registry, TtsConfigEditor ttsEditor, RouletteWheelEditor rouletteWheelEditor, ChatEditor chatEditor, MicrophoneConfigEditor microphoneConfigEditor)
    {
        _registry                    = registry;
        _ttsEditor                   = ttsEditor;
        _rouletteWheelEditor         = rouletteWheelEditor;
        _chatEditor                  = chatEditor;
        _microphoneConfigEditor = microphoneConfigEditor;
    }

    public void Execute(GL _)
    {
        _registry.RegisterSection(_ttsEditor);
        _registry.RegisterSection(_rouletteWheelEditor);
        _registry.RegisterSection(_chatEditor);
        _registry.RegisterSection(_microphoneConfigEditor);
    }
}

namespace PersonaEngine.Lib.ASR.Transcriber;

public class RealtimeOptions
{
    /// <summary>
    ///     Represents the length of the padding segmetents that will be appended to each sement detected by the VAD.
    /// </summary>
    /// <example>
    ///     If the VAD detects a segment of 1200ms starting at offset 200ms, and the <see cref="PaddingDuration" /> is 125ms,
    ///     125ms of the original stream will be appended at the beginning and 125ms at the end of the segment.
    /// </example>
    /// <remarks>
    ///     If the original stream is not long enough to append the padding segments, the padding won't be applied.
    /// </remarks>
    public TimeSpan PaddingDuration { get; set; } = TimeSpan.FromMilliseconds(125);

    /// <summary>
    ///     Represents the minimum length of a segment that will be processed by the transcriptor, if the segment is shorter
    ///     than this value it will be ignored.
    /// </summary>
    public TimeSpan MinTranscriptDuration { get; set; } = TimeSpan.FromMilliseconds(200);

    /// <summary>
    ///     Represents the minimum length of a segment and if it is shorter than this value it will be padded with silence.
    /// </summary>
    /// <example>
    ///     If the segment is 700ms, and the <see cref="MinDurationWithPadding" /> is 1100ms, the segment will be padded with
    ///     400ms of silence (200 ms at the beginning and 200ms at the end).
    /// </example>
    public TimeSpan MinDurationWithPadding { get; set; } = TimeSpan.FromMilliseconds(1100);

    /// <summary>
    ///     If set to true, the recognized segments will be concatenated to form the prompt for newer recognition.
    /// </summary>
    public bool ConcatenateSegmentsToPrompt { get; set; }

    /// <summary>
    ///     Represents the interval at which the transcriptor will process the audio stream.
    /// </summary>
    public TimeSpan ProcessingInterval { get; set; } = TimeSpan.FromMilliseconds(100);

    /// <summary>
    ///     Represents the interval at which the transcriptor will discard silence audio segments.
    /// </summary>
    public TimeSpan SilenceDiscardInterval { get; set; } = TimeSpan.FromSeconds(5);
}

namespace PersonaEngine.Lib.Live2D.Framework.Id;

/// <summary>
///     ID名を管理する。
/// </summary>
public class CubismIdManager
{
    /// <summary>
    ///     登録されているIDのリスト
    /// </summary>
    private readonly List<string> _ids = [];

    /// <summary>
    ///     ID名をリストから登録する。
    /// </summary>
    /// <param name="list">ID名リスト</param>
    public void RegisterIds(List<string> list) { list.ForEach(item => { GetId(item); }); }

    /// <summary>
    ///     ID名からIDを取得する。
    ///     未登録のID名の場合、登録も行う。
    /// </summary>
    /// <param name="item">ID名</param>
    public string GetId(string item)
    {
        if ( _ids.Contains(item) )
        {
            return item;
        }

        _ids.Add(item);

        return item;
    }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

public interface IKokoroVoiceProvider : IAsyncDisposable
{
    Task<VoiceData> GetVoiceAsync(
        string            voiceId,
        CancellationToken cancellationToken = default);

    /// <summary>
    ///     Gets all available voice IDs
    /// </summary>
    /// <returns>List of voice IDs</returns>
    Task<IReadOnlyList<string>> GetAvailableVoicesAsync(
        CancellationToken cancellationToken = default);
}

using System.Diagnostics.Contracts;

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.Common;

internal static class GlUtility
{
    [Pure]
    public static float Clamp(float value, float min, float max)
    {
        return value < min
                   ? min
                   : value > max
                       ? max
                       : value;
    }

    public static void CheckError(this GL gl, string? title = null)
    {
        var error = gl.GetError();
        if ( error == GLEnum.NoError )
        {
            return;
        }

        if ( title != null )
        {
            throw new Exception($"{title}: {error}");
        }

        throw new Exception($"GL.GetError(): {error}");
    }
}

namespace PersonaEngine.Lib.TTS.RVC;

public interface IF0Predictor : IDisposable
{
    void ComputeF0(ReadOnlyMemory<float> wav, Memory<float> f0Output, int length);
}

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Represents a single line of processed subtitles containing multiple words.
/// </summary>
public class SubtitleLine
{
    public SubtitleLine(int segmentIndex, int lineIndexInSegment)
    {
        SegmentIndex       = segmentIndex;
        LineIndexInSegment = lineIndexInSegment;
    }

    public List<SubtitleWordInfo> Words { get; } = new();

    public float TotalWidth { get; set; }

    public float BaselineY { get; set; }

    public int SegmentIndex { get; }

    public int LineIndexInSegment { get; }

    public void AddWord(SubtitleWordInfo word)
    {
        Words.Add(word);
        TotalWidth += word.Size.X;
    }

    public void Clear()
    {
        Words.Clear();
        TotalWidth = 0;
        BaselineY  = 0;
    }
}

namespace PersonaEngine.Lib.Live2D.Framework.Type;

/// <summary>
///     矩形形状(座標・長さはfloat値)を定義するクラス
/// </summary>
public class RectF
{
    /// <summary>
    ///     高さ
    /// </summary>
    public float Height;

    /// <summary>
    ///     幅
    /// </summary>
    public float Width;

    /// <summary>
    ///     左端X座標
    /// </summary>
    public float X;

    /// <summary>
    ///     上端Y座標
    /// </summary>
    public float Y;

    public RectF() { }

    /// <summary>
    ///     引数付きコンストラクタ
    /// </summary>
    /// <param name="x">左端X座標</param>
    /// <param name="y">上端Y座標</param>
    /// <param name="w">幅</param>
    /// <param name="h">高さ</param>
    public RectF(float x, float y, float w, float h)
    {
        X      = x;
        Y      = y;
        Width  = w;
        Height = h;
    }

    /// <summary>
    ///     矩形に値をセットする
    /// </summary>
    /// <param name="r">矩形のインスタンス</param>
    public void SetRect(RectF r)
    {
        X      = r.X;
        Y      = r.Y;
        Width  = r.Width;
        Height = r.Height;
    }

    /// <summary>
    ///     矩形中央を軸にして縦横を拡縮する
    /// </summary>
    /// <param name="w">幅方向に拡縮する量</param>
    /// <param name="h">高さ方向に拡縮する量</param>
    public void Expand(float w, float h)
    {
        X      -= w;
        Y      -= h;
        Width  += w * 2.0f;
        Height += h * 2.0f;
    }

    /// <summary>
    ///     矩形中央のX座標を取得する
    /// </summary>
    public float GetCenterX() { return X + 0.5f * Width; }

    /// <summary>
    ///     矩形中央のY座標を取得する
    /// </summary>
    public float GetCenterY() { return Y + 0.5f * Height; }

    /// <summary>
    ///     右端のX座標を取得する
    /// </summary>
    public float GetRight() { return X + Width; }

    /// <summary>
    ///     下端のY座標を取得する
    /// </summary>
    public float GetBottom() { return Y + Height; }
}

using System.Threading.Channels;

using Microsoft.SemanticKernel;

using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;

namespace PersonaEngine.Lib.LLM;

public record VisualChatMessage(string Query, ReadOnlyMemory<byte> ImageData);

public interface IChatEngine : IDisposable
{
    Task<CompletionReason> GetStreamingChatResponseAsync(
        IConversationContext                 context,
        ChannelWriter<IOutputEvent> outputWriter,
        Guid                        turnId,
        Guid                        sessionId,
        PromptExecutionSettings?    executionSettings = null,
        CancellationToken           cancellationToken = default);
}

public interface IVisualChatEngine : IDisposable
{
    IAsyncEnumerable<string> GetStreamingChatResponseAsync(
        VisualChatMessage        userInput,
        PromptExecutionSettings? executionSettings = null,
        CancellationToken        cancellationToken = default);
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for model loading and management
/// </summary>
public interface IModelProvider : IAsyncDisposable
{
    /// <summary>
    ///     Gets a model by type
    /// </summary>
    /// <param name="modelType">Type of model</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>Model resource</returns>
    Task<ModelResource> GetModelAsync(
        ModelType         modelType,
        CancellationToken cancellationToken = default);
}

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Audio.Player;

/// <summary>
///     Manages audio buffers for playback.
/// </summary>
public interface IAudioBufferManager : IAsyncDisposable
{
    /// <summary>
    ///     Gets the number of audio buffers in the queue.
    /// </summary>
    int BufferCount { get; }

    bool ProducerCompleted { get; }

    /// <summary>
    ///     Enqueues audio segments for playback.
    /// </summary>
    /// <param name="audioSegments">Audio segments to enqueue.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    Task EnqueueSegmentsAsync(IAsyncEnumerable<AudioSegment> audioSegments, CancellationToken cancellationToken);

    /// <summary>
    ///     Tries to get the next audio buffer from the queue.
    /// </summary>
    /// <param name="buffer">The audio buffer if available.</param>
    /// <param name="timeoutMs">Timeout in milliseconds.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>True if a buffer was retrieved, false otherwise.</returns>
    bool TryGetNextBuffer(out (Memory<float> Data, AudioSegment Segment) buffer, int timeoutMs, CancellationToken cancellationToken);

    /// <summary>
    ///     Clears all buffers from the queue.
    /// </summary>
    void Clear();
}

// using System.Threading.Channels;
//
// using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
// using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;
// using PersonaEngine.Lib.TTS.Synthesis;
//
// namespace PersonaEngine.Lib.Audio.Player;
//
// public class AudioPlaybackEventArgs : EventArgs
// {
//     public AudioPlaybackEventArgs(AudioSegment segment) { Segment = segment; }
//
//     public AudioSegment Segment { get; }
// }
//
// public interface IStreamingAudioPlayer : IAsyncDisposable
// {
//     Task StartPlaybackAsync(IAsyncEnumerable<AudioSegment> audioSegments, CancellationToken cancellationToken = default);
//
//     public Task StopPlaybackAsync();
// }
//
// public interface IStreamingAudioPlayerHost
// {
//     float CurrentTime { get; }
//
//     PlayerState State { get; }
//
//     event EventHandler<AudioPlaybackEventArgs>? OnPlaybackStarted;
//
//     event EventHandler<AudioPlaybackEventArgs>? OnPlaybackCompleted;
// }
//
// public interface IAggregatedStreamingAudioPlayer : IStreamingAudioPlayer
// {
//     IReadOnlyCollection<IStreamingAudioPlayer> Players { get; }
//
//     void AddPlayer(IStreamingAudioPlayer player);
//
//     bool RemovePlayer(IStreamingAudioPlayer player);
// }

using System.Text.Json.Serialization;

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Lexicon entry representing pronunciation for a word
/// </summary>
public class LexiconEntry
{
    /// <summary>
    ///     Simple phoneme string (for entries with only one pronunciation)
    /// </summary>
    public string? Simple { get; set; }

    /// <summary>
    ///     Phoneme strings by part-of-speech tag
    /// </summary>
    public Dictionary<string, string>? ByTag { get; set; }

    /// <summary>
    ///     Whether this is a simple entry
    /// </summary>
    [JsonIgnore]
    public bool IsSimple => ByTag == null;

    /// <summary>
    ///     Gets phoneme string for a specific tag
    /// </summary>
    public string? GetForTag(string? tag)
    {
        if ( IsSimple )
        {
            return Simple;
        }

        if ( tag != null && ByTag != null && ByTag.TryGetValue(tag, out var value) )
        {
            return value;
        }

        if ( ByTag != null && ByTag.TryGetValue("DEFAULT", out var def) )
        {
            return def;
        }

        return null;
    }

    /// <summary>
    ///     Whether this entry has a specific tag
    /// </summary>
    public bool HasTag(string tag) { return ByTag != null && ByTag.ContainsKey(tag); }
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Effect;

/// <summary>
///     パーツにまつわる諸々のデータを管理する。
/// </summary>
public record PartData
{
    /// <summary>
    ///     連動するパラメータ
    /// </summary>
    public readonly List<PartData> Link = [];

    /// <summary>
    ///     パーツID
    /// </summary>
    public required string PartId { get; set; }

    /// <summary>
    ///     パラメータのインデックス
    /// </summary>
    public int ParameterIndex { get; set; }

    /// <summary>
    ///     パーツのインデックス
    /// </summary>
    public int PartIndex { get; set; }

    /// <summary>
    ///     初期化する。
    /// </summary>
    /// <param name="model">初期化に使用するモデル</param>
    public void Initialize(CubismModel model)
    {
        ParameterIndex = model.GetParameterIndex(PartId);
        PartIndex      = model.GetPartIndex(PartId);

        model.SetParameterValue(ParameterIndex, 1);
    }
}

using System.Numerics;

namespace PersonaEngine.Lib.Live2D.Framework.Physics;

public record CubismPhysicsObj
{
    public MetaObj Meta { get; set; }

    public List<PhysicsSetting> PhysicsSettings { get; set; }

    public record MetaObj
    {
        public EffectiveForce EffectiveForces { get; set; }

        public float Fps { get; set; }

        public int PhysicsSettingCount { get; set; }

        public int TotalInputCount { get; set; }

        public int TotalOutputCount { get; set; }

        public int VertexCount { get; set; }

        public record EffectiveForce
        {
            public Vector2 Gravity { get; set; }

            public Vector2 Wind { get; set; }
        }
    }

    public record PhysicsSetting
    {
        public NormalizationObj Normalization { get; set; }

        public List<InputObj> Input { get; set; }

        public List<OutputObj> Output { get; set; }

        public List<Vertice> Vertices { get; set; }

        public record NormalizationObj
        {
            public PositionObj Position { get; set; }

            public PositionObj Angle { get; set; }

            public record PositionObj
            {
                public float Minimum { get; set; }

                public float Maximum { get; set; }

                public float Default { get; set; }
            }
        }

        public record InputObj
        {
            public float Weight { get; set; }

            public bool Reflect { get; set; }

            public string Type { get; set; }

            public SourceObj Source { get; set; }

            public record SourceObj
            {
                public string Id { get; set; }
            }
        }

        public record OutputObj
        {
            public int VertexIndex { get; set; }

            public float Scale { get; set; }

            public float Weight { get; set; }

            public DestinationObj Destination { get; set; }

            public string Type { get; set; }

            public bool Reflect { get; set; }

            public record DestinationObj
            {
                public string Id { get; set; }
            }
        }

        public record Vertice
        {
            public float Mobility { get; set; }

            public float Delay { get; set; }

            public float Acceleration { get; set; }

            public float Radius { get; set; }

            public Vector2 Position { get; set; }
        }
    }
}

namespace PersonaEngine.Lib.Configuration;

/// <summary>
///     Configuration for the subtitle renderer
/// </summary>
public class SubtitleOptions
{
    public string Font { get; set; } = "DynaPuff_Condensed-Bold.ttf";

    public int FontSize { get; set; } = 72;

    public string Color { get; set; } = "#FFFFFF";

    public string HighlightColor { get; set; } = "#4FC3F7";

    public int BottomMargin { get; set; } = 50;

    public int SideMargin { get; set; } = 100;

    public float InterSegmentSpacing { get; set; } = 16f;

    public int MaxVisibleLines { get; set; } = 6;

    public float AnimationDuration { get; set; } = 0.3f;

    public int StrokeThickness { get; set; } = 3;

    public int Width { get; set; } = 1920;

    public int Height { get; set; } = 1080;
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

/// <summary>
///     モーション再生終了コールバック関数定義
/// </summary>
public delegate void FinishedMotionCallback(CubismModel model, ACubismMotion self);

/// <summary>
///     イベントのコールバックに登録できる関数の型情報
/// </summary>
/// <param name="eventValue">発火したイベントの文字列データ</param>
/// <param name="customData">コールバックに返される登録時に指定されたデータ</param>
public delegate void CubismMotionEventFunction(CubismUserModel? customData, string eventValue);

/// <summary>
///     モーションカーブのセグメントの評価関数。
/// </summary>
/// <param name="points">モーションカーブの制御点リスト</param>
/// <param name="time">評価する時間[秒]</param>
public delegate float csmMotionSegmentEvaluationFunction(CubismMotionPoint[] points, int start, float time);

/// <summary>
///     モーションの優先度定数
/// </summary>
public enum MotionPriority
{
    PriorityNone = 0,

    PriorityIdle = 1,

    PriorityLow = 2,

    PriorityNormal = 3,

    PriorityForce = 4
}

/// <summary>
///     表情パラメータ値の計算方式
/// </summary>
public enum ExpressionBlendType
{
    /// <summary>
    ///     加算
    /// </summary>
    Add = 0,

    /// <summary>
    ///     乗算
    /// </summary>
    Multiply = 1,

    /// <summary>
    ///     上書き
    /// </summary>
    Overwrite = 2
}

/// <summary>
///     表情のパラメータ情報の構造体。
/// </summary>
public record ExpressionParameter
{
    /// <summary>
    ///     パラメータID
    /// </summary>
    public required string ParameterId { get; set; }

    /// <summary>
    ///     パラメータの演算種類
    /// </summary>
    public ExpressionBlendType BlendType { get; set; }

    /// <summary>
    ///     値
    /// </summary>
    public float Value { get; set; }
}

/// <summary>
///     モーションカーブの種類。
/// </summary>
public enum CubismMotionCurveTarget
{
    /// <summary>
    ///     モデルに対して
    /// </summary>
    Model,

    /// <summary>
    ///     パラメータに対して
    /// </summary>
    Parameter,

    /// <summary>
    ///     パーツの不透明度に対して
    /// </summary>
    PartOpacity
}

/// <summary>
///     モーションカーブのセグメントの種類。
/// </summary>
public enum CubismMotionSegmentType
{
    /// <summary>
    ///     リニア
    /// </summary>
    Linear = 0,

    /// <summary>
    ///     ベジェ曲線
    /// </summary>
    Bezier = 1,

    /// <summary>
    ///     ステップ
    /// </summary>
    Stepped = 2,

    /// <summary>
    ///     インバースステップ
    /// </summary>
    InverseStepped = 3
}

/// <summary>
///     モーションカーブの制御点。
/// </summary>
public struct CubismMotionPoint
{
    /// <summary>
    ///     時間[秒]
    /// </summary>
    public float Time;

    /// <summary>
    ///     値
    /// </summary>
    public float Value;
}

/// <summary>
///     モーションカーブのセグメント。
/// </summary>
public record CubismMotionSegment
{
    /// <summary>
    ///     最初のセグメントへのインデックス
    /// </summary>
    public int BasePointIndex;

    /// <summary>
    ///     使用する評価関数
    /// </summary>
    public csmMotionSegmentEvaluationFunction Evaluate;

    /// <summary>
    ///     セグメントの種類
    /// </summary>
    public CubismMotionSegmentType SegmentType;
}

/// <summary>
///     モーションカーブ。
/// </summary>
public record CubismMotionCurve
{
    /// <summary>
    ///     最初のセグメントのインデックス
    /// </summary>
    public int BaseSegmentIndex;

    /// <summary>
    ///     フェードインにかかる時間[秒]
    /// </summary>
    public float FadeInTime;

    /// <summary>
    ///     フェードアウトにかかる時間[秒]
    /// </summary>
    public float FadeOutTime;

    /// <summary>
    ///     カーブのID
    /// </summary>
    public string Id;

    /// <summary>
    ///     セグメントの個数
    /// </summary>
    public int SegmentCount;

    /// <summary>
    ///     カーブの種類
    /// </summary>
    public CubismMotionCurveTarget Type;
}

/// <summary>
///     イベント。
/// </summary>
public record CubismMotionEvent
{
    public float FireTime;

    public string Value;
}

/// <summary>
///     モーションデータ。
/// </summary>
public record CubismMotionData
{
    /// <summary>
    ///     カーブの個数
    /// </summary>
    public int CurveCount;

    /// <summary>
    ///     カーブのリスト
    /// </summary>
    public CubismMotionCurve[] Curves;

    /// <summary>
    ///     モーションの長さ[秒]
    /// </summary>
    public float Duration;

    /// <summary>
    ///     UserDataの個数
    /// </summary>
    public int EventCount;

    /// <summary>
    ///     イベントのリスト
    /// </summary>
    public CubismMotionEvent[] Events;

    /// <summary>
    ///     フレームレート
    /// </summary>
    public float Fps;

    /// <summary>
    ///     ループするかどうか
    /// </summary>
    public bool Loop;

    /// <summary>
    ///     ポイントのリスト
    /// </summary>
    public CubismMotionPoint[] Points;

    /// <summary>
    ///     セグメントのリスト
    /// </summary>
    public CubismMotionSegment[] Segments;
}

namespace PersonaEngine.Lib.Configuration;

public record RouletteWheelOptions
{
    // Font and Text Settings
    public string Font { get; set; } = "DynaPuff_Condensed-Bold.ttf";

    public int FontSize { get; set; } = 24;

    public string TextColor { get; set; } = "#FFFFFF";

    public float TextScale { get; set; } = 1f;

    public int TextStroke { get; set; } = 2;

    public bool AdaptiveText { get; set; } = true;

    public bool RadialTextOrientation { get; set; } = true;

    // Wheel Configuration
    public string[] SectionLabels { get; set; } = [];

    public float SpinDuration { get; set; } = 8f;

    public float MinRotations { get; set; } = 5f;

    public float WheelSizePercentage { get; set; } = 1.0f;

    // Viewport and Position
    public int Width { get; set; } = 1080;

    public int Height { get; set; } = 1080;

    public string PositionMode { get; set; } = "Anchored"; // "Absolute", "Percentage", or "Anchored"

    public string ViewportAnchor { get; set; } = "Center"; // One of the ViewportAnchor enum values

    public float PositionXPercentage { get; set; } = 0.5f;

    public float PositionYPercentage { get; set; } = 0.5f;

    public float AnchorOffsetX { get; set; } = 0f;

    public float AnchorOffsetY { get; set; } = 0f;

    public float AbsolutePositionX { get; set; } = 0f;

    public float AbsolutePositionY { get; set; } = 0f;

    // State and Animation
    public bool Enabled { get; set; } = false;

    public float RotationDegrees { get; set; } = 0;

    public bool AnimateToggle { get; set; } = true;

    public float AnimationDuration { get; set; } = 0.5f;
}

namespace PersonaEngine.Lib.Audio.Player;

/// <summary>
///     Interface for controlling audio playback scheduling and timing.
/// </summary>
public interface IPlaybackController : IDisposable
{
    /// <summary>
    ///     Gets the current playback time in seconds.
    /// </summary>
    float CurrentTime { get; }

    /// <summary>
    ///     Gets the total number of samples that have been scheduled for playback.
    /// </summary>
    long TotalSamplesPlayed { get; }

    /// <summary>
    ///     Gets the current latency in milliseconds.
    /// </summary>
    double CurrentLatencyMs { get; }

    /// <summary>
    ///     Gets whether latency information is available.
    /// </summary>
    bool HasLatencyInformation { get; }

    /// <summary>
    ///     Event raised when playback time changes significantly.
    /// </summary>
    event EventHandler<float> TimeChanged;

    /// <summary>
    ///     Event raised when latency changes significantly.
    /// </summary>
    event EventHandler<double> LatencyChanged;

    /// <summary>
    ///     Resets the controller to its initial state.
    /// </summary>
    void Reset();

    /// <summary>
    ///     Updates the latency information.
    /// </summary>
    /// <param name="latencyMs">The latency in milliseconds.</param>
    void UpdateLatency(double latencyMs);

    /// <summary>
    ///     Schedules a packet of audio data for playback.
    /// </summary>
    /// <param name="samplesPerChannel">The number of samples per channel in the packet.</param>
    /// <param name="sampleRate">The sample rate of the audio data.</param>
    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
    /// <returns>
    ///     A task that completes when the packet is scheduled, with a boolean indicating whether the packet should be
    ///     sent.
    /// </returns>
    Task<bool> SchedulePlaybackAsync(int samplesPerChannel, int sampleRate, CancellationToken cancellationToken);
}

namespace PersonaEngine.Lib.Live2D.Framework;

public static class CubismLog
{
    public static void CubismLogPrintln(LogLevel level, string head, string fmt, params object?[] args)
    {
        var data = $"[CSM] {head} {string.Format(fmt, args)}";
        if ( level < CubismFramework.GetLoggingLevel() )
        {
            return;
        }

        CubismFramework.CoreLogFunction(data);
    }

    public static void Verbose(string fmt, params object?[] args) { CubismLogPrintln(LogLevel.Verbose, "[V]", fmt, args); }

    public static void Debug(string fmt, params object?[] args) { CubismLogPrintln(LogLevel.Debug, "[D]", fmt, args); }

    public static void Info(string fmt, params object?[] args) { CubismLogPrintln(LogLevel.Info, "[I]", fmt, args); }

    public static void Warning(string fmt, params object?[] args) { CubismLogPrintln(LogLevel.Warning, "[W]", fmt, args); }

    public static void Error(string fmt, params object?[] args) { CubismLogPrintln(LogLevel.Error, "[E]", fmt, args); }
}

namespace PersonaEngine.Lib.Live2D.Framework.Motion;

public record CubismMotionObj
{
    public MetaObj Meta { get; set; }

    public List<Curve> Curves { get; set; }

    public List<UserDataObj> UserData { get; set; }

    public record MetaObj
    {
        public float Duration { get; set; }

        public bool Loop { get; set; }

        public bool AreBeziersRestricted { get; set; }

        public int CurveCount { get; set; }

        public float Fps { get; set; }

        public int TotalSegmentCount { get; set; }

        public int TotalPointCount { get; set; }

        public float? FadeInTime { get; set; }

        public float? FadeOutTime { get; set; }

        public int UserDataCount { get; set; }

        public int TotalUserDataSize { get; set; }
    }

    public record Curve
    {
        public float? FadeInTime { get; set; }

        public float? FadeOutTime { get; set; }

        public List<float> Segments { get; set; }

        public string Target { get; set; }

        public string Id { get; set; }
    }

    public record UserDataObj
    {
        public float Time { get; set; }

        public string Value { get; set; }
    }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Result of audio synthesis
/// </summary>
public class AudioData
{
    public AudioData(Memory<float> samples, ReadOnlyMemory<long> phonemeTimings)
    {
        Samples        = samples;
        PhonemeTimings = phonemeTimings;
    }

    /// <summary>
    ///     Audio samples
    /// </summary>
    public Memory<float> Samples { get; }

    /// <summary>
    ///     Durations for phoneme timing
    /// </summary>
    public ReadOnlyMemory<long> PhonemeTimings { get; }
}

using OpenAI.Chat;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

public class InteractionTurn
{
    public InteractionTurn(Guid turnId, IEnumerable<string> participantIds, DateTimeOffset startTime, DateTimeOffset? endTime, IEnumerable<ChatMessage> messages, bool wasInterrupted)
    {
        TurnId         = turnId;
        ParticipantIds = participantIds.ToList().AsReadOnly();
        StartTime      = startTime;
        EndTime        = endTime;
        Messages       = messages.ToList();
        WasInterrupted = wasInterrupted;
    }

    private InteractionTurn(Guid turnId, IEnumerable<string> participantIds, DateTimeOffset startTime, IEnumerable<ChatMessage> messages)
    {
        TurnId         = turnId;
        ParticipantIds = participantIds.ToList().AsReadOnly();
        StartTime      = startTime;
        EndTime        = null;
        Messages       = messages.Select(m => new ChatMessage(m.MessageId, m.ParticipantId, m.ParticipantName, m.Text, m.Timestamp, m.IsPartial, m.Role)).ToList();
        WasInterrupted = false;
    }

    public Guid TurnId { get; }

    public IReadOnlyList<string> ParticipantIds { get; }

    public DateTimeOffset StartTime { get; }

    public DateTimeOffset? EndTime { get; internal set; }

    public List<ChatMessage> Messages { get; }

    public bool WasInterrupted { get; internal set; }

    internal InteractionTurn CreateSnapshot() { return new InteractionTurn(TurnId, ParticipantIds, StartTime, Messages); }
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

public interface IAudioProgressEvent : IOutputEvent { }

public record AudioPlaybackEndedEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp, CompletionReason FinishReason) : IOutputEvent { }

public record AudioPlaybackStartedEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp) : IOutputEvent { }

public record AudioChunkPlaybackStartedEvent(
    Guid           SessionId,
    Guid?          TurnId,
    DateTimeOffset Timestamp,
    AudioSegment   Chunk
) : BaseOutputEvent(SessionId, TurnId, Timestamp), IAudioProgressEvent;

public record AudioChunkPlaybackEndedEvent(
    Guid           SessionId,
    Guid?          TurnId,
    DateTimeOffset Timestamp,
    AudioSegment   Chunk
) : BaseOutputEvent(SessionId, TurnId, Timestamp), IAudioProgressEvent;

public record AudioPlaybackProgressEvent(
    Guid           SessionId,
    Guid?          TurnId,
    DateTimeOffset Timestamp,
    TimeSpan       CurrentPlaybackTime
) : BaseOutputEvent(SessionId, TurnId, Timestamp), IAudioProgressEvent;

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.LLM;

public interface ITextFilter
{
    int Priority { get; }

    ValueTask<TextFilterResult> ProcessAsync(string text, CancellationToken cancellationToken = default);

    ValueTask PostProcessAsync(TextFilterResult textFilterResult, AudioSegment segment, CancellationToken cancellationToken = default);
}

public record TextFilterResult
{
    public string ProcessedText { get; set; } = string.Empty;

    public Dictionary<string, object> Metadata { get; set; } = new();
}

using System.Numerics;

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     UI theme configuration data
/// </summary>
public class UiTheme
{
    public Vector4 TextColor { get; set; } = new(1.0f, 1.0f, 1.0f, 1.0f);

    public float WindowRounding { get; set; } = 10.0f;

    public float FrameRounding { get; set; } = 5.0f;

    public Vector2 WindowPadding { get; set; } = new(8.0f, 8.0f);

    public Vector2 FramePadding { get; set; } = new(5.0f, 3.5f);

    public Vector2 ItemSpacing { get; set; } = new(5.0f, 4.0f);

    // All style properties from TOML
    public float Alpha { get; set; } = 1.0f;

    public float DisabledAlpha { get; set; } = 0.1000000014901161f;

    public float WindowBorderSize { get; set; } = 0.0f;

    public Vector2 WindowMinSize { get; set; } = new(30.0f, 30.0f);

    public Vector2 WindowTitleAlign { get; set; } = new(0.5f, 0.5f);

    public string WindowMenuButtonPosition { get; set; } = "Right";

    public float ChildRounding { get; set; } = 5.0f;

    public float ChildBorderSize { get; set; } = 1.0f;

    public float PopupRounding { get; set; } = 10.0f;

    public float PopupBorderSize { get; set; } = 0.0f;

    public float FrameBorderSize { get; set; } = 0.0f;

    public Vector2 ItemInnerSpacing { get; set; } = new(5.0f, 5.0f);

    public Vector2 CellPadding { get; set; } = new(4.0f, 2.0f);

    public float IndentSpacing { get; set; } = 5.0f;

    public float ColumnsMinSpacing { get; set; } = 5.0f;

    public float ScrollbarSize { get; set; } = 15.0f;

    public float ScrollbarRounding { get; set; } = 9.0f;

    public float GrabMinSize { get; set; } = 15.0f;

    public float GrabRounding { get; set; } = 5.0f;

    public float TabRounding { get; set; } = 5.0f;

    public float TabBorderSize { get; set; } = 0.0f;

    public float TabCloseButtonMinWidthSelected { get; set; } = 0.0f;

    public float TabCloseButtonMinWidthUnselected { get; set; } = 0.0f;

    public string ColorButtonPosition { get; set; } = "Right";

    public Vector2 ButtonTextAlign { get; set; } = new(0.5f, 0.5f);

    public Vector2 SelectableTextAlign { get; set; } = new(0.0f, 0.0f);

    // All colors from TOML
    public Vector4 TextDisabledColor { get; set; } = new(1.0f, 1.0f, 1.0f, 0.3605149984359741f);

    public Vector4 WindowBgColor { get; set; } = new(0.098f, 0.098f, 0.098f, 1.0f);

    public Vector4 ChildBgColor { get; set; } = new(1.0f, 0.0f, 0.0f, 0.0f);

    public Vector4 PopupBgColor { get; set; } = new(0.098f, 0.098f, 0.098f, 1.0f);

    public Vector4 BorderColor { get; set; } = new(0.424f, 0.380f, 0.573f, 0.54935622215271f);

    public Vector4 BorderShadowColor { get; set; } = new(0.0f, 0.0f, 0.0f, 0.0f);

    public Vector4 FrameBgColor { get; set; } = new(0.157f, 0.157f, 0.157f, 1.0f);

    public Vector4 FrameBgHoveredColor { get; set; } = new(0.380f, 0.424f, 0.573f, 0.5490196347236633f);

    public Vector4 FrameBgActiveColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 TitleBgColor { get; set; } = new(0.098f, 0.098f, 0.098f, 1.0f);

    public Vector4 TitleBgActiveColor { get; set; } = new(0.098f, 0.098f, 0.098f, 1.0f);

    public Vector4 TitleBgCollapsedColor { get; set; } = new(0.259f, 0.259f, 0.259f, 0.0f);

    public Vector4 MenuBarBgColor { get; set; } = new(0.0f, 0.0f, 0.0f, 0.0f);

    public Vector4 ScrollbarBgColor { get; set; } = new(0.157f, 0.157f, 0.157f, 0.0f);

    public Vector4 ScrollbarGrabColor { get; set; } = new(0.157f, 0.157f, 0.157f, 1.0f);

    public Vector4 ScrollbarGrabHoveredColor { get; set; } = new(0.235f, 0.235f, 0.235f, 1.0f);

    public Vector4 ScrollbarGrabActiveColor { get; set; } = new(0.294f, 0.294f, 0.294f, 1.0f);

    public Vector4 CheckMarkColor { get; set; } = new(0.294f, 0.294f, 0.294f, 1.0f);

    public Vector4 SliderGrabColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 SliderGrabActiveColor { get; set; } = new(0.816f, 0.773f, 0.965f, 0.5490196347236633f);

    public Vector4 ButtonColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 ButtonHoveredColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 ButtonActiveColor { get; set; } = new(0.816f, 0.773f, 0.965f, 0.5490196347236633f);

    public Vector4 HeaderColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 HeaderHoveredColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 HeaderActiveColor { get; set; } = new(0.816f, 0.773f, 0.965f, 0.5490196347236633f);

    public Vector4 SeparatorColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 SeparatorHoveredColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 SeparatorActiveColor { get; set; } = new(0.816f, 0.773f, 0.965f, 0.5490196347236633f);

    public Vector4 ResizeGripColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 ResizeGripHoveredColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 ResizeGripActiveColor { get; set; } = new(0.816f, 0.773f, 0.965f, 0.5490196347236633f);

    public Vector4 TabColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 TabHoveredColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 TabActiveColor { get; set; } = new(0.816f, 0.773f, 0.965f, 0.5490196347236633f);

    public Vector4 TabUnfocusedColor { get; set; } = new(0.0f, 0.451f, 1.0f, 0.0f);

    public Vector4 TabUnfocusedActiveColor { get; set; } = new(0.133f, 0.259f, 0.424f, 0.0f);

    public Vector4 PlotLinesColor { get; set; } = new(0.294f, 0.294f, 0.294f, 1.0f);

    public Vector4 PlotLinesHoveredColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 PlotHistogramColor { get; set; } = new(0.620f, 0.576f, 0.769f, 0.5490196347236633f);

    public Vector4 PlotHistogramHoveredColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 TableHeaderBgColor { get; set; } = new(0.188f, 0.188f, 0.200f, 1.0f);

    public Vector4 TableBorderStrongColor { get; set; } = new(0.424f, 0.380f, 0.573f, 0.5490196347236633f);

    public Vector4 TableBorderLightColor { get; set; } = new(0.424f, 0.380f, 0.573f, 0.2918455004692078f);

    public Vector4 TableRowBgColor { get; set; } = new(0.0f, 0.0f, 0.0f, 0.0f);

    public Vector4 TableRowBgAltColor { get; set; } = new(1.0f, 1.0f, 1.0f, 0.03433477878570557f);

    public Vector4 TextSelectedBgColor { get; set; } = new(0.737f, 0.694f, 0.886f, 0.5490196347236633f);

    public Vector4 DragDropTargetColor { get; set; } = new(1.0f, 1.0f, 0.0f, 0.8999999761581421f);

    public Vector4 NavWindowingHighlightColor { get; set; } = new(1.0f, 1.0f, 1.0f, 0.699999988079071f);

    public Vector4 NavWindowingDimBgColor { get; set; } = new(0.8f, 0.8f, 0.8f, 0.2000000029802322f);

    public Vector4 ModalWindowDimBgColor { get; set; } = new(0.8f, 0.8f, 0.8f, 0.3499999940395355f);
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Data structure for active operations with progress tracking
/// </summary>
public class ActiveOperation
{
    public ActiveOperation(string id, string name)
    {
        Id                 = id;
        Name               = name;
        Progress           = 0f;
        CancellationSource = new CancellationTokenSource();
    }

    public string Id { get; }

    public string Name { get; }

    public float Progress { get; set; }

    public CancellationTokenSource CancellationSource { get; }
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

public abstract class OpenGLApi
{
    public readonly int GL_ACTIVE_TEXTURE = 0x84E0;

    public readonly int GL_ARRAY_BUFFER = 0x8892;

    public readonly int GL_ARRAY_BUFFER_BINDING = 0x8894;

    public readonly int GL_BLEND = 0x0BE2;

    public readonly int GL_BLEND_DST_ALPHA = 0x80CA;

    public readonly int GL_BLEND_DST_RGB = 0x80C8;

    public readonly int GL_BLEND_SRC_ALPHA = 0x80CB;

    public readonly int GL_BLEND_SRC_RGB = 0x80C9;

    public readonly int GL_CCW = 0x0901;

    public readonly int GL_CLAMP_TO_EDGE = 0x812F;

    public readonly int GL_COLOR_ATTACHMENT0 = 0x8CE0;

    public readonly int GL_COLOR_BUFFER_BIT = 0x4000;

    public readonly int GL_COLOR_WRITEMASK = 0x0C23;

    public readonly int GL_COMPILE_STATUS = 0x8B81;

    public readonly int GL_CULL_FACE = 0x0B44;

    public readonly int GL_CURRENT_PROGRAM = 0x8B8D;

    public readonly int GL_DEPTH_BUFFER_BIT = 0x0100;

    public readonly int GL_DEPTH_TEST = 0x0B71;

    public readonly int GL_DST_COLOR = 0x0306;

    public readonly int GL_ELEMENT_ARRAY_BUFFER = 0x8893;

    public readonly int GL_ELEMENT_ARRAY_BUFFER_BINDING = 0x8895;

    public readonly int GL_FALSE = 0;

    public readonly int GL_FLOAT = 0x1406;

    public readonly int GL_FRAGMENT_SHADER = 0x8B30;

    public readonly int GL_FRAMEBUFFER = 0x8D40;

    public readonly int GL_FRAMEBUFFER_BINDING = 0x8CA6;

    public readonly int GL_FRONT_FACE = 0x0B46;

    public readonly int GL_INFO_LOG_LENGTH = 0x8B84;

    public readonly int GL_LINEAR = 0x2601;

    public readonly int GL_LINEAR_MIPMAP_LINEAR = 0x2703;

    public readonly int GL_LINK_STATUS = 0x8B82;

    public readonly int GL_ONE = 1;

    public readonly int GL_ONE_MINUS_SRC_ALPHA = 0x0303;

    public readonly int GL_ONE_MINUS_SRC_COLOR = 0x0301;

    public readonly int GL_RGBA = 0x1908;

    public readonly int GL_SCISSOR_TEST = 0x0C11;

    public readonly int GL_SRC_ALPHA = 0x0302;

    public readonly int GL_STATIC_DRAW = 0x88E4;

    public readonly int GL_STENCIL_TEST = 0x0B90;

    public readonly int GL_TEXTURE_2D = 0x0DE1;

    public readonly int GL_TEXTURE_BINDING_2D = 0x8069;

    public readonly int GL_TEXTURE_MAG_FILTER = 0x2800;

    public readonly int GL_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE;

    public readonly int GL_TEXTURE_MIN_FILTER = 0x2801;

    public readonly int GL_TEXTURE_WRAP_S = 0x2802;

    public readonly int GL_TEXTURE_WRAP_T = 0x2803;

    public readonly int GL_TEXTURE0 = 0x84C0;

    public readonly int GL_TEXTURE1 = 0x84C1;

    public readonly int GL_TRIANGLES = 0x0004;

    public readonly int GL_UNSIGNED_BYTE = 0x1401;

    public readonly int GL_UNSIGNED_SHORT = 0x1403;

    public readonly int GL_VALIDATE_STATUS = 0x8B83;

    public readonly int GL_VERTEX_ATTRIB_ARRAY_ENABLED = 0x8622;

    public readonly int GL_VERTEX_SHADER = 0x8B31;

    public readonly int GL_VIEWPORT = 0x0BA2;

    public readonly int GL_ZERO = 0;

    public abstract bool AlwaysClear { get; }

    public abstract bool IsES2 { get; }

    public abstract bool IsPhoneES2 { get; }

    public abstract void Viewport(int x, int y, int w, int h);

    public abstract void ClearColor(float r, float g, float b, float a);

    public abstract void Clear(int bit);

    public abstract void Enable(int bit);

    public abstract void Disable(int bit);

    public abstract void EnableVertexAttribArray(int index);

    public abstract void DisableVertexAttribArray(int index);

    public abstract void GetIntegerv(int bit, out int data);

    public abstract void GetIntegerv(int bit, int[] data);

    public abstract void ActiveTexture(int bit);

    public abstract void GetVertexAttribiv(int index, int bit, out int data);

    public abstract bool IsEnabled(int bit);

    public abstract void GetBooleanv(int bit, bool[] data);

    public abstract void UseProgram(int index);

    public abstract void FrontFace(int data);

    public abstract void ColorMask(bool a, bool b, bool c, bool d);

    public abstract void BindBuffer(int bit, int index);

    public abstract void BindTexture(int bit, int index);

    public abstract void BlendFuncSeparate(int a, int b, int c, int d);

    public abstract void DeleteProgram(int index);

    public abstract int GetAttribLocation(int index, string attr);

    public abstract int GetUniformLocation(int index, string uni);

    public abstract void Uniform1i(int index, int data);

    public abstract void VertexAttribPointer(int index, int length, int type, bool b, int size, nint arr);

    public abstract void Uniform4f(int index, float a, float b, float c, float d);

    public abstract void UniformMatrix4fv(int index, int length, bool b, float[] data);

    public abstract int CreateProgram();

    public abstract void AttachShader(int a, int b);

    public abstract void DeleteShader(int index);

    public abstract void DetachShader(int index, int data);

    public abstract int CreateShader(int type);

    public abstract void ShaderSource(int a, string source);

    public abstract void CompileShader(int index);

    public abstract unsafe void GetShaderiv(int index, int type, int* length);

    public abstract void GetShaderInfoLog(int index, out string log);

    public abstract void LinkProgram(int index);

    public abstract unsafe void GetProgramiv(int index, int type, int* length);

    public abstract void GetProgramInfoLog(int index, out string log);

    public abstract void ValidateProgram(int index);

    public abstract void DrawElements(int type, int count, int type1, nint arry);

    public abstract void BindVertexArrayOES(int data);

    public abstract void TexParameterf(int type, int type1, float value);

    public abstract void BindFramebuffer(int type, int data);

    public abstract int GenTexture();

    public abstract void TexImage2D(int type, int a, int type1, int w, int h, int size, int type2, int type3, IntPtr data);

    public abstract void TexParameteri(int a, int b, int c);

    public abstract int GenFramebuffer();

    public abstract void FramebufferTexture2D(int a, int b, int c, int buff, int data);

    public abstract void DeleteTexture(int data);

    public abstract void DeleteFramebuffer(int fb);

    public abstract void BlendFunc(int a, int b);

    public abstract void GetWindowSize(out int w, out int h);

    public abstract void GenerateMipmap(int a);

    public abstract void ClearDepthf(float data);

    public abstract int GetError();

    public abstract int GenBuffer();

    public abstract void BufferData(int type, int v1, nint v2, int type1);

    public abstract int GenVertexArray();

    public abstract void BindVertexArray(int vertexArray);
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Options for configuring the TTS memory cache behavior
/// </summary>
public class TtsCacheOptions
{
    /// <summary>
    ///     Maximum number of items to store in the cache (0 = unlimited)
    /// </summary>
    public int MaxItems { get; set; } = 1000;

    /// <summary>
    ///     Time after which cache items expire (null = never expire)
    /// </summary>
    public TimeSpan? ItemExpiration { get; set; } = TimeSpan.FromHours(1);

    /// <summary>
    ///     Whether to collect performance metrics
    /// </summary>
    public bool CollectMetrics { get; set; } = true;
}

namespace PersonaEngine.Lib.ASR.VAD;

internal class SileroConstants
{
    public const int BatchSize = 512;

    public const int ContextSize = 64;

    public const int StateSize = 256;

    public const int OutputSize = 1;

    public const int SampleRate = 16000;
}

using PersonaEngine.Lib.Audio.Player;
using PersonaEngine.Lib.Live2D.App;

namespace PersonaEngine.Lib.Live2D.Behaviour;

/// <summary>
///     Generic interface for services that animate Live2D model parameters
///     based on various input sources (audio, text content, emotions, etc.).
/// </summary>
public interface ILive2DAnimationService : IDisposable
{
    /// <summary>
    ///     Starts the animation processing. This activates the service to
    ///     begin listening for events and modifying parameters.
    /// </summary>
    void Start(LAppModel model);

    /// <summary>
    ///     Stops the animation processing. Parameter values might be reset to neutral
    ///     or held, depending on the implementation.
    /// </summary>
    void Stop();

    /// <summary>
    ///     Updates the animation state, typically called once per frame.
    ///     This method calculates and applies parameter values to the Live2D model
    ///     based on the current input data and timing.
    /// </summary>
    /// <param name="deltaTime">The time elapsed since the last frame/update, in seconds.</param>
    void Update(float deltaTime);
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for a TTS caching mechanism
/// </summary>
public interface ITtsCache : IAsyncDisposable
{
    /// <summary>
    ///     Gets or adds an item to the cache
    /// </summary>
    /// <typeparam name="T">The type of the item</typeparam>
    /// <param name="key">The cache key</param>
    /// <param name="valueFactory">Factory function to create the value if not found</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>The cached or newly created value</returns>
    Task<T> GetOrAddAsync<T>(
        string                           key,
        Func<CancellationToken, Task<T>> valueFactory,
        CancellationToken                cancellationToken = default) where T : class;

    /// <summary>
    ///     Removes an item from the cache
    /// </summary>
    /// <param name="key">The cache key to remove</param>
    void Remove(string key);

    /// <summary>
    ///     Clears all items from the cache
    /// </summary>
    void Clear();

    /// <summary>
    ///     Gets the current cache statistics
    /// </summary>
    /// <returns>Tuple with current stats</returns>
    (int Size, long Hits, long Misses, long Evictions) GetStatistics();
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for converting text to phonemes
/// </summary>
public interface IPhonemizer : IDisposable
{
    /// <summary>
    ///     Converts text to phoneme representation
    /// </summary>
    /// <param name="text">Input text</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>Phoneme result</returns>
    Task<PhonemeResult> ToPhonemesAsync(
        string            text,
        CancellationToken cancellationToken = default);
}

namespace PersonaEngine.Lib.Live2D.Framework.Model;

public record CubismModelUserDataObj
{
    public MetaObj Meta { get; set; }

    public List<UserDataObj> UserData { get; set; }

    public record MetaObj
    {
        public int UserDataCount { get; set; }

        public int TotalUserDataSize { get; set; }
    }

    public record UserDataObj
    {
        public string Target { get; set; }

        public string Id { get; set; }

        public string Value { get; set; }
    }
}

using System.Threading.Channels;

using PersonaEngine.Lib.Audio;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;

public interface IInputAdapter : IAsyncDisposable
{
    Guid AdapterId { get; }

    ParticipantInfo Participant { get; }

    ValueTask InitializeAsync(Guid sessionId, ChannelWriter<IInputEvent> inputWriter, CancellationToken cancellationToken);

    ValueTask StartAsync(CancellationToken cancellationToken);

    ValueTask StopAsync(CancellationToken cancellationToken);
}

public interface IAudioInputAdapter : IInputAdapter
{
    IAwaitableAudioSource GetAudioSource();
}

public interface ITextInputAdapter : IInputAdapter { }

using System.ComponentModel;

namespace PersonaEngine.Lib.Live2D.Framework;

public record ModelSettingObj
{
    public FileReference FileReferences { get; set; }

    public List<HitArea> HitAreas { get; set; }

    public Dictionary<string, float> Layout { get; set; }

    public List<Parameter> Groups { get; set; }

    public record FileReference
    {
        public string Moc { get; set; }

        public List<string> Textures { get; set; }

        public string Physics { get; set; }

        public string Pose { get; set; }

        public string DisplayInfo { get; set; }

        public List<Expression> Expressions { get; set; }

        public Dictionary<string, List<Motion>> Motions { get; set; }

        public string UserData { get; set; }

        public record Expression
        {
            public string Name { get; set; }

            public string File { get; set; }
        }

        public record Motion
        {
            public string File { get; set; }

            public string Sound { get; set; }

            [DefaultValue(-1f)] public float FadeInTime { get; set; }

            [DefaultValue(-1f)] public float FadeOutTime { get; set; }
        }
    }

    public record HitArea
    {
        public string Id { get; set; }

        public string Name { get; set; }
    }

    public record Parameter
    {
        public string Name { get; set; }

        public List<string> Ids { get; set; }
    }
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;

namespace PersonaEngine.Lib.Configuration;

public record AvatarAppConfig
{
    public WindowConfiguration Window { get; set; } = new();

    public LlmOptions Llm { get; set; } = new();

    public TtsConfiguration Tts { get; set; } = new();

    public AsrConfiguration Asr { get; set; } = new();

    public MicrophoneConfiguration Microphone { get; set; } = new();

    public SubtitleOptions Subtitle { get; set; } = new();

    public Live2DOptions Live2D { get; set; } = new();

    public SpoutConfiguration[] SpoutConfigs { get; set; } = [];

    public VisionConfig Vision { get; set; } = new();

    public RouletteWheelOptions RouletteWheel { get; set; } = new();

    public ConversationOptions Conversation { get; set; } = new();

    // This would need to be seperate per configured conversation session
    public ConversationContextOptions ConversationContext { get; set; } = new();
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents an audio source that can be awaited for new samples and can be flushed.
/// </summary>
public interface IAwaitableAudioSource : IAudioSource
{
    bool IsFlushed { get; }

    /// <summary>
    ///     Waits for new samples to be available.
    /// </summary>
    /// <param name="sampleCount">The sample count to wait for.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The number of times the wait was performed.</returns>
    Task WaitForInitializationAsync(CancellationToken cancellationToken);

    /// <summary>
    ///     Waits for the source to have at least the specified number of samples.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    Task WaitForNewSamplesAsync(long sampleCount, CancellationToken cancellationToken);

    /// <summary>
    ///     Waits for the source to be at least the specified duration.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns></returns>
    Task WaitForNewSamplesAsync(TimeSpan minimumDuration, CancellationToken cancellationToken);

    /// <summary>
    ///     Flushes the stream, indicating that no more data will be written.
    /// </summary>
    void Flush();
}

namespace PersonaEngine.Lib.TTS.Profanity;

public class ProfanityTestData
{
    public static IEnumerable<(string Sentence, bool ExpectedIsProfane)> TestCases => new List<(string, bool)> {
                                                                                // Explicit Profanity (Expanded)
                                                                                ("You're a cunt and everyone knows it.", true),
                                                                                ("Holy shit, that's one massive cock!", true),
                                                                                ("Bullshit detector going off the charts.", true),
                                                                                ("Eat a bag of dicks, you worthless twat.", true),
                                                                                ("This nazi propaganda makes me sick.", true),

                                                                                // Innocent Phrases (Expanded)
                                                                                ("Mansfield's historical documents", false),
                                                                                ("Cockburn Harbour shipping schedule", false),
                                                                                ("Shitake mushroom risotto recipe", false),
                                                                                ("Cummins engine specifications", false),
                                                                                ("Titmouse spotted in woodland area", false),

                                                                                // Foreign Words (Expanded)
                                                                                ("Fart means 'speed' in Norwegian", false),
                                                                                ("Kuk (Polish for 'rooster') symbolism", false),
                                                                                ("Concha (Spanish seashell) collection", false),
                                                                                ("Puta madre (Spanish idiom) explanation", false),
                                                                                ("Fica (Italian fig fruit) exports", false),

                                                                                // Contextual Slurs (Expanded)
                                                                                ("Digital rectal examination needed", false),
                                                                                ("Spasticity in cerebral palsy", false),
                                                                                ("Midget submarine technology", true),
                                                                                ("Gyp payment systems", true),
                                                                                ("Retardant chemical formula", false),

                                                                                // Split Evasion (Expanded)
                                                                                ("F.u.c.k.e.r.y in progress", true),
                                                                                ("B-i-t-c-h please!", true),
                                                                                ("S H I T B R I C K S", true),

                                                                                // Homophones/Near-Misses (Expanded)
                                                                                ("Pass the Bass ale", false),
                                                                                ("Duck dynasty documentary", false),
                                                                                ("Master debater competition", false),
                                                                                ("Cockpit voice recorder data", false),
                                                                                ("Therapist ass.ociation meeting", false),

                                                                                // Implicit Offense (Expanded)
                                                                                ("That's so jewish of you", true),
                                                                                ("Gypsy lifestyle portrayal", true),
                                                                                ("Wheelchair-bound vocabulary", true),
                                                                                ("Tranny fluid replacement", true), // Mechanical context but problematic
                                                                                ("Pikey market traders", true),

                                                                                // Technical Terms (New Category)
                                                                                ("Cumulus cloud formation", false),
                                                                                ("Scunthorpe town council", false),
                                                                                ("Cumbustion engine manual", false), // Typo but should pass
                                                                                ("Clbuttic encoding error", false),
                                                                                ("Rectum measurement device", false),

                                                                                // Cultural References (Expanded)
                                                                                ("Kick the bucket list", false),
                                                                                ("Donkey's years since", false),
                                                                                ("The bee's knees cocktail", false),
                                                                                ("Cat got your tongue?", false),
                                                                                ("Bob's your uncle", false),

                                                                                // Digital Contexts (New Category)
                                                                                ("http://cunt.ly/shortener", true),
                                                                                ("User@analytics.com", false),
                                                                                ("WTF_Championship results", true),
                                                                                ("dickbutt.png filename", true),
                                                                                ("NSFW_Content_Warning", true),

                                                                                // Word Boundaries (New Category)
                                                                                ("Assassin's creed gameplay", false),
                                                                                ("Classicass movie review", true), // "classic ass"
                                                                                ("Butternut squash soup", false),
                                                                                ("Masshole driver behavior", true), // MA + asshole
                                                                                ("Grasshopper infestation", false),

                                                                                // Multi-Language Mix (New Category)
                                                                                ("Foda-se (Portuguese) phrasebook", true),
                                                                                ("Merde alors! (French)", true),
                                                                                ("Chingado (Spanish) cultural study", true),
                                                                                ("Kurwa (Polish) linguistics", true),
                                                                                ("Scheiße (German) dictionary", true),

                                                                                // Historical Terms (New Category)
                                                                                ("Niggardly medieval records", false),
                                                                                ("Spastic colony archives", true),
                                                                                ("Retardation in horology", false),
                                                                                ("Gay 90s fashion exhibit", false),
                                                                                ("Oriental rug cleaning", false),

                                                                                // Corporate/Product Names
                                                                                ("Buttplug.io developer tools", true),
                                                                                ("CumEx Financial scandal", true),
                                                                                ("Hitler's Poodle pub", true),
                                                                                ("DikFm radio station", true),
                                                                                ("FckngKlm apparel brand", true),

                                                                                // Poetic/Literary
                                                                                ("Wherefore art thou, Romeo?", false),
                                                                                ("Shakespeare's sonnet 69", false),
                                                                                ("Byron's dark fuckëdness", true),
                                                                                ("Whitman's body electric", false),
                                                                                ("Plath's bell jar metaphor", false),

                                                                                // Youth Vernacular
                                                                                ("That's sus cap no bussin", false),
                                                                                ("Lit af fam no kizzy", false),
                                                                                ("She's being extra salty", false),
                                                                                ("Ghosted his toxic ass", true),
                                                                                ("Daddy chill vibes only", true)
                                                                                };
}

using Microsoft.ML.OnnxRuntime;

namespace PersonaEngine.Lib.ASR.VAD;

internal class SileroInferenceState(OrtIoBinding binding)
{
    /// Array for storing the context + input

    public float[] State { get; set; } = new float[SileroConstants.StateSize];

    // The state for the next inference
    public float[] PendingState { get; set; } = new float[SileroConstants.StateSize];

    public float[] Output { get; set; } = new float[SileroConstants.OutputSize];

    public OrtIoBinding Binding { get; } = binding;
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;

public record struct BargeInContext(
    ConversationOptions ConversationOptions,
    ConversationState CurrentState,
    IInputEvent       InputEvent,
    Guid              SessionId,
    Guid?             TurnId
);

public interface IBargeInStrategy
{
    bool ShouldAllowBargeIn(BargeInContext context);
}

using System.Numerics;
using System.Runtime.InteropServices;

namespace PersonaEngine.Lib.UI.RouletteWheel;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct VertexPositionTexture
{
    public readonly Vector3 Position;

    public readonly Vector2 TextureCoordinate;

    public VertexPositionTexture(Vector3 position, Vector2 textureCoordinate)
    {
        Position          = position;
        TextureCoordinate = textureCoordinate;
    }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

public interface IAudioFilter
{
    int Priority { get; }

    void Process(AudioSegment audioSegment);
}

using PersonaEngine.Lib.Live2D.Framework.Math;
using PersonaEngine.Lib.Live2D.Framework.Type;

namespace PersonaEngine.Lib.Live2D.Framework.Rendering;

public unsafe class CubismClippingContext(CubismClippingManager manager, int* clippingDrawableIndices, int clipCount)
{
    /// <summary>
    ///     このクリッピングで、クリッピングされる全ての描画オブジェクトの囲み矩形（毎回更新）
    /// </summary>
    public RectF AllClippedDrawRect = new();

    /// <summary>
    ///     このマスクが割り当てられるレンダーテクスチャ（フレームバッファ）やカラーバッファのインデックス
    /// </summary>
    public int BufferIndex;

    /// <summary>
    ///     このマスクにクリップされる描画オブジェクトのリスト
    /// </summary>
    public List<int> ClippedDrawableIndexList = [];

    /// <summary>
    ///     クリッピングマスクの数
    /// </summary>
    public int ClippingIdCount = clipCount;

    /// <summary>
    ///     クリッピングマスクのIDリスト
    /// </summary>
    public int* ClippingIdList = clippingDrawableIndices;

    /// <summary>
    ///     現在の描画状態でマスクの準備が必要ならtrue
    /// </summary>
    public bool IsUsing;

    /// <summary>
    ///     マスク用チャンネルのどの領域にマスクを入れるか(View座標-1..1, UVは0..1に直す)
    /// </summary>
    public RectF LayoutBounds = new();

    /// <summary>
    ///     RGBAのいずれのチャンネルにこのクリップを配置するか(0:R , 1:G , 2:B , 3:A)
    /// </summary>
    public int LayoutChannelIndex = 0;

    /// <summary>
    ///     描画オブジェクトの位置計算結果を保持する行列
    /// </summary>
    public CubismMatrix44 MatrixForDraw = new();

    /// <summary>
    ///     マスクの位置計算結果を保持する行列
    /// </summary>
    public CubismMatrix44 MatrixForMask = new();

    public CubismClippingManager Manager { get; } = manager;

    /// <summary>
    ///     このマスクにクリップされる描画オブジェクトを追加する
    /// </summary>
    /// <param name="drawableIndex">クリッピング対象に追加する描画オブジェクトのインデックス</param>
    public void AddClippedDrawable(int drawableIndex) { ClippedDrawableIndexList.Add(drawableIndex); }
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Events.Input;

public record SttSegmentRecognizing(
    Guid           SessionId,
    DateTimeOffset Timestamp,
    string         ParticipantId,
    string         PartialTranscript,
    TimeSpan       Duration
) : IInputEvent
{
    public Guid? TurnId { get; } = null;
}

public record SttSegmentRecognized(
    Guid           SessionId,
    DateTimeOffset Timestamp,
    string         ParticipantId,
    string         FinalTranscript,
    TimeSpan       Duration,
    float?         Confidence
) : IInputEvent
{
    public Guid? TurnId { get; } = null;
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Details of the audio source header.
/// </summary>
public class AudioSourceHeader
{
    /// <summary>
    ///     Gets the number of channels in the current wave file.
    /// </summary>
    public ushort Channels { get; set; }

    /// <summary>
    ///     Gets the Sample Rate in the current wave file.
    /// </summary>
    public uint SampleRate { get; set; }

    /// <summary>
    ///     Gets the Bits Per Sample in the current wave file.
    /// </summary>
    public ushort BitsPerSample { get; set; }
}

namespace PersonaEngine.Lib.ASR.VAD;

public class SileroVadOptions(string modelPath)
{
    public string ModelPath { get; set; } = modelPath;

    public float ThresholdGap { get; set; } = 0.15f;

    public float Threshold { get; set; } = 0.5f;
}

using PersonaEngine.Lib.Live2D.Framework;

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     アプリケーションクラス。
///     Cubism SDK の管理を行う。
/// </summary>
public static class LAppDefine
{
    // 画面
    public const float ViewScale = 1.0f;

    public const float ViewMaxScale = 2.0f;

    public const float ViewMinScale = 0.8f;

    public const float ViewLogicalLeft = -1.0f;

    public const float ViewLogicalRight = 1.0f;

    public const float ViewLogicalBottom = -1.0f;

    public const float ViewLogicalTop = -1.0f;

    public const float ViewLogicalMaxLeft = -2.0f;

    public const float ViewLogicalMaxRight = 2.0f;

    public const float ViewLogicalMaxBottom = -2.0f;

    public const float ViewLogicalMaxTop = 2.0f;

    // 外部定義ファイル(json)と合わせる
    public const string MotionGroupIdle = "Idle"; // アイドリング

    public const string MotionGroupTapBody = "TapBody"; // 体をタップしたとき

    // 外部定義ファイル(json)と合わせる
    public const string HitAreaNameHead = "Head";

    public const string HitAreaNameBody = "Body";

    // MOC3の整合性検証オプション
    public const bool MocConsistencyValidationEnable = true;

    // Frameworkから出力するログのレベル設定
    public const LogLevel CubismLoggingLevel = LogLevel.Verbose;
}

using System.Numerics;

namespace PersonaEngine.Lib.Live2D.Framework.Physics;

/// <summary>
///     物理演算の適用先の種類。
/// </summary>
public enum CubismPhysicsTargetType
{
    /// <summary>
    ///     パラメータに対して適用
    /// </summary>
    CubismPhysicsTargetType_Parameter
}

/// <summary>
///     物理演算の入力の種類。
/// </summary>
public enum CubismPhysicsSource
{
    /// <summary>
    ///     X軸の位置から
    /// </summary>
    CubismPhysicsSource_X,

    /// <summary>
    ///     Y軸の位置から
    /// </summary>
    CubismPhysicsSource_Y,

    /// <summary>
    ///     角度から
    /// </summary>
    CubismPhysicsSource_Angle
}

/// <summary>
///     物理演算で使用する外部の力。
/// </summary>
public record PhysicsJsonEffectiveForces
{
    /// <summary>
    ///     重力
    /// </summary>
    public Vector2 Gravity;

    /// <summary>
    ///     風
    /// </summary>
    public Vector2 Wind;
}

/// <summary>
///     物理演算のパラメータ情報。
/// </summary>
public record CubismPhysicsParameter
{
    /// <summary>
    ///     パラメータID
    /// </summary>
    public required string Id;

    /// <summary>
    ///     適用先の種類
    /// </summary>
    public CubismPhysicsTargetType TargetType;
}

/// <summary>
///     物理演算の正規化情報。
/// </summary>
public record CubismPhysicsNormalization
{
    /// <summary>
    ///     デフォルト値
    /// </summary>
    public float Default;

    /// <summary>
    ///     最小値
    /// </summary>
    public float Maximum;

    /// <summary>
    ///     最大値
    /// </summary>
    public float Minimum;
}

/// <summary>
///     物理演算の演算に使用する物理点の情報。
/// </summary>
public record CubismPhysicsParticle
{
    /// <summary>
    ///     加速度
    /// </summary>
    public float Acceleration;

    /// <summary>
    ///     遅れ
    /// </summary>
    public float Delay;

    /// <summary>
    ///     現在かかっている力
    /// </summary>
    public Vector2 Force;

    /// <summary>
    ///     初期位置
    /// </summary>
    public Vector2 InitialPosition;

    /// <summary>
    ///     最後の重力
    /// </summary>
    public Vector2 LastGravity;

    /// <summary>
    ///     最後の位置
    /// </summary>
    public Vector2 LastPosition;

    /// <summary>
    ///     動きやすさ
    /// </summary>
    public float Mobility;

    /// <summary>
    ///     現在の位置
    /// </summary>
    public Vector2 Position;

    /// <summary>
    ///     距離
    /// </summary>
    public float Radius;

    /// <summary>
    ///     現在の速度
    /// </summary>
    public Vector2 Velocity;
}

/// <summary>
///     物理演算の物理点の管理。
/// </summary>
public record CubismPhysicsSubRig
{
    /// <summary>
    ///     入力の最初のインデックス
    /// </summary>
    public int BaseInputIndex;

    /// <summary>
    ///     出力の最初のインデックス
    /// </summary>
    public int BaseOutputIndex;

    /// <summary>
    ///     /物理点の最初のインデックス
    /// </summary>
    public int BaseParticleIndex;

    /// <summary>
    ///     入力の個数
    /// </summary>
    public int InputCount;

    /// <summary>
    ///     正規化された角度
    /// </summary>
    public required CubismPhysicsNormalization NormalizationAngle;

    /// <summary>
    ///     正規化された位置
    /// </summary>
    public required CubismPhysicsNormalization NormalizationPosition;

    /// <summary>
    ///     出力の個数
    /// </summary>
    public int OutputCount;

    /// <summary>
    ///     物理点の個数
    /// </summary>
    public int ParticleCount;
}

/// <summary>
///     正規化されたパラメータの取得関数の宣言。
/// </summary>
/// <param name="targetTranslation">演算結果の移動値</param>
/// <param name="targetAngle">演算結果の角度</param>
/// <param name="value">パラメータの値</param>
/// <param name="parameterMinimumValue">パラメータの最小値</param>
/// <param name="parameterMaximumValue">パラメータの最大値</param>
/// <param name="parameterDefaultValue">パラメータのデフォルト値</param>
/// <param name="normalizationPosition">正規化された位置</param>
/// <param name="normalizationAngle">正規化された角度</param>
/// <param name="isInverted">値が反転されているか？</param>
/// <param name="weight">重み</param>
public delegate void NormalizedPhysicsParameterValueGetter(
    ref Vector2                targetTranslation,
    ref float                  targetAngle,
    float                      value,
    float                      parameterMinimumValue,
    float                      parameterMaximumValue,
    float                      parameterDefaultValue,
    CubismPhysicsNormalization normalizationPosition,
    CubismPhysicsNormalization normalizationAngle,
    bool                       isInverted,
    float                      weight
);

/// <summary>
///     物理演算の値の取得関数の宣言。
/// </summary>
/// <param name="translation">移動値</param>
/// <param name="particles">物理点のリスト</param>
/// <param name="particleIndex"></param>
/// <param name="isInverted">値が反転されているか？</param>
/// <param name="parentGravity">重力</param>
/// <returns>値</returns>
public delegate float PhysicsValueGetter(
    Vector2                 translation,
    CubismPhysicsParticle[] particles,
    int                     currentParticleIndex,
    int                     particleIndex,
    bool                    isInverted,
    Vector2                 parentGravity
);

/// <summary>
///     物理演算のスケールの取得関数の宣言。
/// </summary>
/// <param name="translationScale">移動値のスケール</param>
/// <param name="angleScale">角度のスケール</param>
/// <returns>スケール値</returns>
public delegate float PhysicsScaleGetter(Vector2 translationScale, float angleScale);

/// <summary>
///     物理演算の入力情報。
/// </summary>
public record CubismPhysicsInput
{
    /// <summary>
    ///     正規化されたパラメータ値の取得関数
    /// </summary>
    public NormalizedPhysicsParameterValueGetter GetNormalizedParameterValue;

    /// <summary>
    ///     値が反転されているかどうか
    /// </summary>
    public bool Reflect;

    /// <summary>
    ///     入力元のパラメータ
    /// </summary>
    public required CubismPhysicsParameter Source;

    /// <summary>
    ///     入力元のパラメータのインデックス
    /// </summary>
    public int SourceParameterIndex;

    /// <summary>
    ///     入力の種類
    /// </summary>
    public CubismPhysicsSource Type;

    /// <summary>
    ///     重み
    /// </summary>
    public float Weight;
}

/// <summary>
///     物理演算の出力情報。
/// </summary>
public record CubismPhysicsOutput
{
    /// <summary>
    ///     角度のスケール
    /// </summary>
    public float AngleScale;

    /// <summary>
    ///     出力先のパラメータ
    /// </summary>
    public required CubismPhysicsParameter Destination;

    /// <summary>
    ///     出力先のパラメータのインデックス
    /// </summary>
    public int DestinationParameterIndex;

    /// <summary>
    ///     物理演算のスケール値の取得関数
    /// </summary>
    public PhysicsScaleGetter GetScale;

    /// <summary>
    ///     物理演算の値の取得関数
    /// </summary>
    public PhysicsValueGetter GetValue;

    /// <summary>
    ///     値が反転されているかどうか
    /// </summary>
    public bool Reflect;

    /// <summary>
    ///     移動値のスケール
    /// </summary>
    public Vector2 TranslationScale;

    /// <summary>
    ///     出力の種類
    /// </summary>
    public CubismPhysicsSource Type;

    /// <summary>
    ///     最小値を下回った時の値
    /// </summary>
    public float ValueBelowMinimum;

    /// <summary>
    ///     最大値をこえた時の値
    /// </summary>
    public float ValueExceededMaximum;

    /// <summary>
    ///     振り子のインデックス
    /// </summary>
    public int VertexIndex;

    /// <summary>
    ///     重み
    /// </summary>
    public float Weight;
}

/// <summary>
///     物理演算のデータ。
/// </summary>
public record CubismPhysicsRig
{
    /// <summary>
    ///     物理演算動作FPS
    /// </summary>
    public float Fps;

    /// <summary>
    ///     重力
    /// </summary>
    public Vector2 Gravity;

    /// <summary>
    ///     物理演算の入力のリスト
    /// </summary>
    public required CubismPhysicsInput[] Inputs;

    /// <summary>
    ///     物理演算の出力のリスト
    /// </summary>
    public required CubismPhysicsOutput[] Outputs;

    /// <summary>
    ///     物理演算の物理点のリスト
    /// </summary>
    public required CubismPhysicsParticle[] Particles;

    /// <summary>
    ///     物理演算の物理点の管理のリスト
    /// </summary>
    public required CubismPhysicsSubRig[] Settings;

    /// <summary>
    ///     物理演算の物理点の個数
    /// </summary>
    public int SubRigCount;

    /// <summary>
    ///     風
    /// </summary>
    public Vector2 Wind;
}

namespace PersonaEngine.Lib.Audio.Player;

/// <summary>
///     Builder for VBAN packets.
/// </summary>
public interface IVBANPacketBuilder
{
    /// <summary>
    ///     Builds a VBAN packet from audio data.
    /// </summary>
    byte[] BuildPacket(ReadOnlyMemory<float> audioData, int sampleRate, int samplesPerChannel, int channels);
}

namespace PersonaEngine.Lib.Live2D.Framework.Physics;

/// <summary>
///     physics3.jsonのコンテナ。
/// </summary>
public record CubismPhysicsJson
{
    public const string Position = "Position";

    public const string X = "X";

    public const string Y = "Y";

    public const string Angle = "Angle";

    public const string Type = "Type";

    public const string Id = "Id";

    // Meta
    public const string Meta = "Meta";

    public const string EffectiveForces = "EffectiveForces";

    public const string TotalInputCount = "TotalInputCount";

    public const string TotalOutputCount = "TotalOutputCount";

    public const string PhysicsSettingCount = "PhysicsSettingCount";

    public const string Gravity = "Gravity";

    public const string Wind = "Wind";

    public const string VertexCount = "VertexCount";

    public const string Fps = "Fps";

    // PhysicsSettings
    public const string PhysicsSettings = "PhysicsSettings";

    public const string Normalization = "Normalization";

    public const string Minimum = "Minimum";

    public const string Maximum = "Maximum";

    public const string Default = "Default";

    public const string Reflect = "Reflect";

    public const string Weight = "Weight";

    // Input
    public const string Input = "Input";

    public const string Source = "Source";

    // Output
    public const string Output = "Output";

    public const string Scale = "Scale";

    public const string VertexIndex = "VertexIndex";

    public const string Destination = "Destination";

    // Particle
    public const string Vertices = "Vertices";

    public const string Mobility = "Mobility";

    public const string Delay = "Delay";

    public const string Radius = "Radius";

    public const string Acceleration = "Acceleration";
}

namespace PersonaEngine.Lib.Configuration;

public class Live2DOptions
{
    public string ModelPath { get; set; } = "Resources/Live2D/Avatars";

    public string ModelName { get; set; } = "angel";

    public int Width { get; set; } = 1920;

    public int Height { get; set; } = 1080;
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;
using PersonaEngine.Lib.Core.Conversation.Implementations.Context;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Session;

public interface IConversationSessionFactory
{
    IConversationSession CreateSession(ConversationContext context, ConversationOptions? options = null, Guid? sessionId = null);
}

namespace PersonaEngine.Lib.ASR.VAD;

public class VadDetectorOptions
{
    public TimeSpan MinSpeechDuration { get; set; } = TimeSpan.FromMilliseconds(150);

    public TimeSpan MinSilenceDuration { get; set; } = TimeSpan.FromMilliseconds(150);
}

using System.Text.Json.Serialization;

using PersonaEngine.Lib.Live2D.Framework.Model;
using PersonaEngine.Lib.Live2D.Framework.Motion;
using PersonaEngine.Lib.Live2D.Framework.Physics;

namespace PersonaEngine.Lib.Live2D.Framework;

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(ModelSettingObj))]
public partial class ModelSettingObjContext : JsonSerializerContext { }

[JsonSerializable(typeof(CubismMotionObj))]
public partial class CubismMotionObjContext : JsonSerializerContext { }

[JsonSerializable(typeof(CubismModelUserDataObj))]
public partial class CubismModelUserDataObjContext : JsonSerializerContext { }

[JsonSerializable(typeof(CubismPhysicsObj))]
public partial class CubismPhysicsObjContext : JsonSerializerContext { }

namespace PersonaEngine.Lib.Live2D.Framework;

public static class CubismDefaultParameterId
{
    // パーツID
    public const string HitAreaPrefix = "HitArea";

    public const string HitAreaHead = "Head";

    public const string HitAreaBody = "Body";

    public const string PartsIdCore = "Parts01Core";

    public const string PartsArmPrefix = "Parts01Arm_";

    public const string PartsArmLPrefix = "Parts01ArmL_";

    public const string PartsArmRPrefix = "Parts01ArmR_";

    // パラメータID
    public const string ParamAngleX = "ParamAngleX";

    public const string ParamAngleY = "ParamAngleY";

    public const string ParamAngleZ = "ParamAngleZ";

    public const string ParamEyeLOpen = "ParamEyeLOpen";

    public const string ParamEyeLSmile = "ParamEyeLSmile";

    public const string ParamEyeROpen = "ParamEyeROpen";

    public const string ParamEyeRSmile = "ParamEyeRSmile";

    public const string ParamEyeBallX = "ParamEyeBallX";

    public const string ParamEyeBallY = "ParamEyeBallY";

    public const string ParamEyeBallForm = "ParamEyeBallForm";

    public const string ParamBrowLY = "ParamBrowLY";

    public const string ParamBrowRY = "ParamBrowRY";

    public const string ParamBrowLX = "ParamBrowLX";

    public const string ParamBrowRX = "ParamBrowRX";

    public const string ParamBrowLAngle = "ParamBrowLAngle";

    public const string ParamBrowRAngle = "ParamBrowRAngle";

    public const string ParamBrowLForm = "ParamBrowLForm";

    public const string ParamBrowRForm = "ParamBrowRForm";

    public const string ParamMouthForm = "ParamMouthForm";

    public const string ParamMouthOpenY = "ParamMouthOpenY";

    public const string ParamCheek = "ParamCheek";

    public const string ParamBodyAngleX = "ParamBodyAngleX";

    public const string ParamBodyAngleY = "ParamBodyAngleY";

    public const string ParamBodyAngleZ = "ParamBodyAngleZ";

    public const string ParamBreath = "ParamBreath";

    public const string ParamArmLA = "ParamArmLA";

    public const string ParamArmRA = "ParamArmRA";

    public const string ParamArmLB = "ParamArmLB";

    public const string ParamArmRB = "ParamArmRB";

    public const string ParamHandL = "ParamHandL";

    public const string ParamHandR = "ParamHandR";

    public const string ParamHairFront = "ParamHairFront";

    public const string ParamHairSide = "ParamHairSide";

    public const string ParamHairBack = "ParamHairBack";

    public const string ParamHairFluffy = "ParamHairFluffy";

    public const string ParamShoulderY = "ParamShoulderY";

    public const string ParamBustX = "ParamBustX";

    public const string ParamBustY = "ParamBustY";

    public const string ParamBaseX = "ParamBaseX";

    public const string ParamBaseY = "ParamBaseY";

    public const string ParamNONE = "NONE";

    public static readonly string ParamVoiceA = "ParamA";

    public static readonly string ParamVoiceI = "ParamI";

    public static readonly string ParamVoiceU = "ParamU";

    public static readonly string ParamVoiceE = "ParamE";

    public static readonly string ParamVoiceO = "ParamO";

    public static readonly string ParamVoiceSilence = "ParamSilence";
}

using PersonaEngine.Lib.ASR.Transcriber;

namespace PersonaEngine.Lib.Audio;

public record RealtimeSpeechTranscriptorOptions : SpeechTranscriptorOptions
{
    /// <summary>
    ///     Gets or sets a value indicating whether to include speech recognizing events.
    /// </summary>
    /// <remarks>
    ///     This events are emitted before the segment is complete and are not final.
    /// </remarks>
    public bool IncludeSpeechRecogizingEvents { get; set; }

    /// <summary>
    ///     Gets or sets a value indicating whether to autodetect the language only once at startup and use it for all the
    ///     segments, instead of autodetecting it for each segment.
    /// </summary>
    public bool AutodetectLanguageOnce { get; set; }
}

using PersonaEngine.Lib.Audio;

namespace PersonaEngine.Lib.ASR.VAD;

/// <summary>
///     Represents a voice activity detection component that can detect voice activity segments in mono-channel audio
///     samples at 16 kHz.
/// </summary>
public interface IVadDetector
{
    /// <summary>
    ///     Detects voice activity segments in the given audio source.
    /// </summary>
    /// <param name="source">The audio source to analyze.</param>
    /// <param name="cancellationToken">The cancellation token to observe.</param>
    IAsyncEnumerable<VadSegment> DetectSegmentsAsync(IAudioSource source, CancellationToken cancellationToken);
}

using System.Runtime.CompilerServices;

namespace PersonaEngine.Lib.Utils;

/// <summary>
///     Thread-safe counter implementation.
/// </summary>
public class AtomicCounter
{
    private uint _value;

    public uint GetValue() { return _value; }

    public uint Increment() { return (uint)Interlocked.Increment(ref Unsafe.As<uint, int>(ref _value)); }
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Event arguments for configuration changes
/// </summary>
public class ConfigurationChangedEventArgs : EventArgs
{
    public enum ChangeType
    {
        Updated,

        Reloaded,

        Saved
    }

    public ConfigurationChangedEventArgs(string sectionKey, ChangeType type)
    {
        SectionKey = sectionKey;
        Type       = type;
    }

    public string SectionKey { get; }

    public ChangeType Type { get; }
}

using Silk.NET.Input;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;

namespace PersonaEngine.Lib.UI;

public interface IRenderComponent : IDisposable
{
    bool UseSpout { get; }

    string SpoutTarget { get; }

    /// <summary>
    ///     Priority of the filter (higher values run first)
    /// </summary>
    int Priority { get; }

    void Update(float deltaTime);

    void Render(float deltaTime);

    void Resize();

    void Initialize(GL gl, IView view, IInputContext input);
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Result of text preprocessing
/// </summary>
public class ProcessedText
{
    public ProcessedText(string normalizedText, IReadOnlyList<string> sentences)
    {
        NormalizedText = normalizedText;
        Sentences      = sentences;
    }

    /// <summary>
    ///     Normalized text
    /// </summary>
    public string NormalizedText { get; }

    /// <summary>
    ///     Segmented sentences
    /// </summary>
    public IReadOnlyList<string> Sentences { get; }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Result of phoneme conversion
/// </summary>
public class PhonemeResult
{
    public PhonemeResult(string phonemes, IReadOnlyList<Token> tokens)
    {
        Phonemes = phonemes;
        Tokens   = tokens;
    }

    /// <summary>
    ///     Phoneme string representation
    /// </summary>
    public string Phonemes { get; }

    /// <summary>
    ///     Tokens with phonetic information
    /// </summary>
    public IReadOnlyList<Token> Tokens { get; }
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Event arguments for editor state changes
/// </summary>
public class EditorStateChangedEventArgs : EventArgs
{
    public EditorStateChangedEventArgs(string stateKey, object oldValue, object newValue)
    {
        StateKey = stateKey;
        OldValue = oldValue;
        NewValue = newValue;
    }

    public string StateKey { get; }

    public object OldValue { get; }

    public object NewValue { get; }
}

using System.Globalization;

namespace PersonaEngine.Lib.ASR.Transcriber;

public static class WhisperNetSupportedLanguage
{
    private static readonly HashSet<string> supportedLanguages = [
        "en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", "pl", "ca", "nl",
        "ar", "sv", "it", "id", "hi", "fi", "vi", "he", "uk", "el", "ms", "cs", "ro",
        "da", "hu", "ta", "no", "th", "ur", "hr", "bg", "lt", "la", "mi", "ml", "cy",
        "sk", "te", "fa", "lv", "bn", "sr", "az", "sl", "kn", "et", "mk", "br", "eu",
        "is", "hy", "ne", "mn", "bs", "kk", "sq", "sw", "gl", "mr", "pa", "si", "km",
        "sn", "yo", "so", "af", "oc", "ka", "be", "tg", "sd", "gu", "am", "yi", "lo",
        "uz", "fo", "ht", "ps", "tk", "nn", "mt", "sa", "lb", "my", "bo", "tl", "mg",
        "as", "tt", "haw", "ln", "ha", "ba", "jw", "su", "yue"
    ];

    public static bool IsSupported(CultureInfo cultureInfo) { return supportedLanguages.Contains(cultureInfo.TwoLetterISOLanguageName); }

    public static IEnumerable<CultureInfo> GetSupportedLanguages() { return supportedLanguages.Select(x => new CultureInfo(x)); }
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Strategies;

 public class AllowBargeInStrategy : IBargeInStrategy
    {
        public bool ShouldAllowBargeIn(BargeInContext context)
        {
            return true;
        }
    }

using PersonaEngine.Lib.ASR.Transcriber;

namespace PersonaEngine.Lib.Audio;

public interface IRealtimeRecognitionEvent { }

public interface IRealtimeTranscriptionSegment : IRealtimeRecognitionEvent
{
    public TranscriptSegment Segment { get; }
}

public class RealtimeSessionStarted(string sessionId) : IRealtimeRecognitionEvent
{
    public string SessionId { get; } = sessionId;
}

public class RealtimeSessionStopped(string sessionId) : IRealtimeRecognitionEvent
{
    public object SessionId { get; } = sessionId;
}

public class RealtimeSessionCanceled(string sessionId) : IRealtimeRecognitionEvent
{
    public object SessionId { get; } = sessionId;
}

public class RealtimeSegmentRecognizing(TranscriptSegment segment, string sessionId) : IRealtimeTranscriptionSegment
{
    public TranscriptSegment Segment { get; } = segment;

    public string SessionId { get; } = sessionId;
}

public class RealtimeSegmentRecognized(TranscriptSegment segment, string sessionId) : IRealtimeTranscriptionSegment
{
    public TranscriptSegment Segment { get; } = segment;

    public string SessionId { get; } = sessionId;
}

using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
///     Extension methods for accessing emotion data in audio segments
/// </summary>
public static class EmotionExtensions
{
    /// <summary>
    ///     Gets the emotions associated with an audio segment
    /// </summary>
    public static IReadOnlyList<EmotionTiming> GetEmotions(this AudioSegment segment, IEmotionService emotionService) { return emotionService.GetEmotions(segment.Id); }
}

using PersonaEngine.Lib.Live2D.Framework.Model;

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

public unsafe class CubismClippingContext_OpenGLES2(
    CubismClippingManager manager,
    CubismModel           model,
    int*                  clippingDrawableIndices,
    int                   clipCount) : CubismClippingContext(manager, clippingDrawableIndices, clipCount) { }

using PersonaEngine.Lib.Audio;

namespace PersonaEngine.Lib.ASR.Transcriber;

/// <summary>
///     Represents a segment of transcribed text.
/// </summary>
public interface ISpeechTranscriptor : IAsyncDisposable
{
    /// <summary>
    ///     Transcribes the given audio stream to segments of text.
    /// </summary>
    /// <param name="source">The audio source to transcribe.</param>
    /// <param name="cancellationToken">The cancellation token to observe.</param>
    /// <returns></returns>
    IAsyncEnumerable<TranscriptSegment> TranscribeAsync(IAudioSource source, CancellationToken cancellationToken);
}

namespace PersonaEngine.Lib.Configuration;

public record LlmOptions
{
    public string TextApiKey { get; set; } = string.Empty;

    public string VisionApiKey { get; set; } = string.Empty;

    public string TextModel { get; set; } = string.Empty;

    public string VisionModel { get; set; } = string.Empty;

    public string TextEndpoint { get; set; } = string.Empty;

    public string VisionEndpoint { get; set; } = string.Empty;
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;
using PersonaEngine.Lib.TTS.Synthesis;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

public record TtsStreamStartEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp) : IOutputEvent { }

public record TtsChunkEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp, AudioSegment Chunk) : IOutputEvent { }

public record TtsStreamEndEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp, CompletionReason FinishReason) : IOutputEvent { }

namespace PersonaEngine.Lib.Configuration;

public record VisionConfig
{
    public string WindowTitle { get; init; } = "Paint";

    public bool Enabled { get; init; } = false;

    public TimeSpan CaptureInterval { get; init; } = TimeSpan.FromSeconds(45);

    public int CaptureMinPixels { get; init; } = 224 * 224;

    public int CaptureMaxPixels { get; init; } = 2048 * 2048;
}

namespace PersonaEngine.Lib.ASR.Transcriber;

/// <summary>
///     Factory for creating instances of <see cref="ISpeechTranscriptorFactory" />.
/// </summary>
public interface ISpeechTranscriptorFactory : IDisposable
{
    /// <summary>
    ///     Creates a new instance of <see cref="ISpeechTranscriptor" />.
    /// </summary>
    /// <returns></returns>
    ISpeechTranscriptor Create(SpeechTranscriptorOptions options);
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Interface for a microphone audio source.
///     Extends IAwaitableAudioSource with recording control methods
///     and device information retrieval.
/// </summary>
public interface IMicrophone : IAwaitableAudioSource
{
    /// <summary>
    ///     Starts capturing audio from the microphone.
    /// </summary>
    void StartRecording();

    /// <summary>
    ///     Stops capturing audio from the microphone.
    /// </summary>
    void StopRecording();

    /// <summary>
    ///     Gets a list of available audio input device names.
    /// </summary>
    /// <returns>An enumerable collection of device names.</returns>
    IEnumerable<string> GetAvailableDevices();
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;

public record ConversationOptions
{
    public BargeInType BargeInType { get; set; } = BargeInType.MinWords;

    public int BargeInMinWords { get; set; } = 2;
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

public interface IConversationCleanupStrategy
{
    bool Cleanup(List<InteractionTurn> history, ConversationContextOptions options, IReadOnlyDictionary<string, ParticipantInfo> participants);
}

namespace PersonaEngine.Lib.TTS.Profanity;

/// <summary>
///     Holds benchmark statistics for a profanity detection run.
/// </summary>
public record BenchmarkResult
{
    public int Total { get; set; }

    public int TruePositives { get; set; }

    public int FalsePositives { get; set; }

    public int TrueNegatives { get; set; }

    public int FalseNegatives { get; set; }

    public double Accuracy { get; set; }

    public double Precision { get; set; }

    public double Recall { get; set; }

    public override string ToString()
    {
        return $"Total: {Total}, TP: {TruePositives}, FP: {FalsePositives}, TN: {TrueNegatives}, FN: {FalseNegatives}, " +
               $"Accuracy: {Accuracy:P2}, Precision: {Precision:P2}, Recall: {Recall:P2}";
    }
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Configuration;

public record ConversationContextOptions
{
    public string? SystemPromptFile { get; set; }

    public string? SystemPrompt { get; set; }

    public string? CurrentContext { get; set; }

    public List<string> Topics { get; set; } = new();
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Phonetic constants used in pronunciation
/// </summary>
public class PhonemeConstants
{
    public PhonemeConstants(bool useBritishEnglish) { UseBritishEnglish = useBritishEnglish; }

    /// <summary>
    ///     Whether to use British English pronunciation
    /// </summary>
    public bool UseBritishEnglish { get; init; }

    /// <summary>
    ///     US-specific tau sounds for flapping rules
    /// </summary>
    public HashSet<char> UsTauSounds { get; } = new("aeiouAEIOUæɑɒɔəɛɪʊʌᵻ");

    /// <summary>
    ///     Currency symbols with their word representations
    /// </summary>
    public Dictionary<char, (string Units, string Subunits)> CurrencyRepresentations { get; } = new() { { '$', ("dollar", "cent") }, { '£', ("pound", "pence") }, { '€', ("euro", "cent") } };

    public Dictionary<string, string> Symbols { get; } = new();
}

namespace PersonaEngine.Lib.Configuration;

public record MicrophoneConfiguration
{
    /// <summary>
    ///     The friendly name of the desired input device.
    ///     If null, empty, or whitespace, the default device (DeviceNumber 0) will be used.
    /// </summary>
    public string? DeviceName { get; init; } = null;
}

using System.Globalization;

using PersonaEngine.Lib.Configuration;

namespace PersonaEngine.Lib.ASR.Transcriber;

public record SpeechTranscriptorOptions
{
    public bool LanguageAutoDetect { get; set; } = true;

    public bool RetrieveTokenDetails { get; set; }

    public CultureInfo Language { get; set; } = CultureInfo.GetCultureInfo("en-us");

    public string? Prompt { get; set; }

    public WhisperConfigTemplate? Template { get; set; }
}

using PersonaEngine.Lib.Audio;

namespace PersonaEngine.Lib.ASR.Transcriber;

public interface IRealtimeSpeechTranscriptor
{
    IAsyncEnumerable<IRealtimeRecognitionEvent> TranscribeAsync(IAwaitableAudioSource source, CancellationToken cancellationToken = default);
}

namespace PersonaEngine.Lib.Configuration;

public class WindowConfiguration
{
    public int Width { get; set; } = 1920;

    public int Height { get; set; } = 1080;

    public string Title { get; set; } = "Avatar Application";

    public bool Fullscreen { get; set; } = false;
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Session;

public interface IConversationOrchestrator : IAsyncDisposable
{
    IConversationSession GetSession(Guid sessionId);

    Task<Guid> StartNewSessionAsync(CancellationToken cancellationToken = default);

    ValueTask StopSessionAsync(Guid sessionId);

    IEnumerable<Guid> GetActiveSessionIds();

    ValueTask StopAllSessionsAsync();

    event EventHandler? SessionsUpdated;
}

using Microsoft.SemanticKernel.ChatCompletion;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

public interface IConversationContext : IDisposable
{
    IReadOnlyDictionary<string, ParticipantInfo> Participants { get; }

    IReadOnlyList<InteractionTurn> History { get; }

    string? CurrentVisualContext { get; }

    InteractionTurn? PendingTurn { get; }

    bool TryAddParticipant(ParticipantInfo participant);

    bool TryRemoveParticipant(string participantId);

    void StartTurn(Guid turnId, IEnumerable<string> participantIds);

    void AppendToTurn(string participantId, string chunk);

    string GetPendingMessageText(string participantId);

    void CompleteTurnPart(string participantId, bool interrupted = false);

    IReadOnlyList<InteractionTurn> GetProjectedHistory();

    public void AbortTurn();

    ChatHistory GetSemanticKernelChatHistory(bool includePendingTurn = true);

    bool TryUpdateMessage(Guid turnId, Guid messageId, string newText);

    bool TryDeleteMessage(Guid turnId, Guid messageId);

    void ApplyCleanupStrategy();

    void ClearHistory();

    event EventHandler? ConversationUpdated;
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Manages the state of the configuration editor UI
/// </summary>
public interface IEditorStateManager
{
    bool HasUnsavedChanges { get; }

    event EventHandler<EditorStateChangedEventArgs> StateChanged;

    void MarkAsChanged(string? sectionKey = null);

    void MarkAsSaved();

    ActiveOperation? GetActiveOperation();

    void RegisterActiveOperation(ActiveOperation operation);

    void ClearActiveOperation(string operationId);
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for text preprocessing
/// </summary>
public interface ITextProcessor
{
    /// <summary>
    ///     Processes raw text for TTS synthesis
    /// </summary>
    /// <param name="text">Input text</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>Processed text with sentences</returns>
    Task<ProcessedText> ProcessAsync(string text, CancellationToken cancellationToken = default);
}

namespace PersonaEngine.Lib.Audio;

/// <summary>
///     Represents a source that can discard frames from the beginning of the audio stream if they are no longer needed.
/// </summary>
public interface IDiscardableAudioSource : IAudioSource
{
    /// <summary>
    ///     Discards a specified number of frames.
    /// </summary>
    /// <param name="count">The number of frames to discard.</param>
    void DiscardFrames(int count);
}

namespace PersonaEngine.Lib.Utils;

/// <summary>
///     Configuration for the ArrayPools used in the library.
/// </summary>
public class ArrayPoolConfig
{
    /// <summary>
    ///     Determines whether arrays should be cleared before being returned to the pool.
    /// </summary>
    /// <remarks>
    ///     Default value is <c>false</c> for performance reasons. If you are working with sensitive data, you may want to set
    ///     this to <c>true</c>.
    /// </remarks>
    public static bool ClearOnReturn { get; set; }
}

using Microsoft.Extensions.Logging;

using Polly;

namespace PersonaEngine.Lib.Logging;

public static class ResilienceKeys
{
    public static readonly ResiliencePropertyKey<Guid> SessionId = new("session-id");

    public static readonly ResiliencePropertyKey<ILogger> Logger = new("logger");
}

namespace PersonaEngine.Lib.Live2D;

public interface ILive2DModel : IDisposable
{
    string ModelId { get; }

    void SetParameter(string paramName, float value);

    void Update(float deltaTime);

    void Draw();

    void SetPosition(float x, float y);

    void SetScale(float scale);

    void SetRotation(float rotation);
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for fallback phoneme generation
/// </summary>
public interface IFallbackPhonemizer : IAsyncDisposable
{
    /// <summary>
    ///     Gets phonemes for a word when the lexicon fails
    /// </summary>
    Task<(string? Phonemes, int? Rating)> GetPhonemesAsync(string word, CancellationToken cancellationToken = default);
}

using System.Numerics;

using FontStashSharp;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Holds the intrinsic properties and calculated state of a single word for rendering.
///     Designed as a struct to potentially reduce GC pressure when dealing with many words,
///     but be mindful of copying costs if passed around extensively by value.
/// </summary>
public struct SubtitleWordInfo
{
    public string Text;

    public Vector2 Size;

    public float AbsoluteStartTime;

    public float Duration;

    public Vector2 Position;

    public float AnimationProgress;

    public FSColor CurrentColor;

    public Vector2 CurrentScale;

    public bool IsActive(float currentTime) { return currentTime >= AbsoluteStartTime && currentTime < AbsoluteStartTime + Duration; }

    public bool IsComplete(float currentTime) { return currentTime >= AbsoluteStartTime + Duration; }

    public bool HasStarted(float currentTime) { return currentTime >= AbsoluteStartTime; }
}

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
/// Service that tracks emotion timings for audio segments
/// </summary>
public interface IEmotionService
{
    /// <summary>
    /// Associates emotion timings with an audio segment
    /// </summary>
    void RegisterEmotions(Guid segmentId, IReadOnlyList<EmotionTiming> emotions);

    /// <summary>
    /// Retrieves emotion timings for an audio segment
    /// </summary>
    IReadOnlyList<EmotionTiming> GetEmotions(Guid segmentId);
}

namespace PersonaEngine.Lib.Live2D.Framework;

/// <summary>
///     メモリアロケーションを抽象化したクラス.
///     メモリ確保・解放処理をプラットフォーム側で実装して
///     フレームワークから呼び出すためのインターフェース。
/// </summary>
public interface ICubismAllocator
{
    /// <summary>
    ///     アラインメント制約なしのヒープ・メモリーを確保します。
    /// </summary>
    /// <param name="size">確保するバイト数</param>
    /// <returns>成功すると割り当てられたメモリのアドレス。 そうでなければ '0'を返す。</returns>
    IntPtr Allocate(int size);

    /// <summary>
    ///     アラインメント制約なしのヒープ・メモリーを解放します。
    /// </summary>
    /// <param name="memory">解放するメモリのアドレス</param>
    void Deallocate(IntPtr memory);

    /// <summary>
    ///     アラインメント制約ありのヒープ・メモリーを確保します。
    /// </summary>
    /// <param name="size">確保するバイト数</param>
    /// <param name="alignment">メモリーブロックのアラインメント幅</param>
    /// <returns>成功すると割り当てられたメモリのアドレス。 そうでなければ '0'を返す。</returns>
    IntPtr AllocateAligned(int size, int alignment);

    /// <summary>
    ///     アラインメント制約ありのヒープ・メモリーを解放します。
    /// </summary>
    /// <param name="alignedMemory">解放するメモリのアドレス</param>
    void DeallocateAligned(IntPtr alignedMemory);
}

namespace PersonaEngine.Lib.ASR.VAD;

/// <summary>
///     Represents a voice activity detection segment.
/// </summary>
public class VadSegment
{
    /// <summary>
    ///     Gets or sets the start time of the segment.
    /// </summary>
    public TimeSpan StartTime { get; set; }

    /// <summary>
    ///     Gets or sets the duration of the segment.
    /// </summary>
    public TimeSpan Duration { get; set; }

    /// <summary>
    ///     Gets or sets a value indicating whether this segment is the last one in the audio stream and can be incomplete.
    /// </summary>
    public bool IsIncomplete { get; set; }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for sentence segmentation
/// </summary>
public interface ISentenceSegmenter
{
    /// <summary>
    ///     Splits text into sentences
    /// </summary>
    /// <param name="text">Input text</param>
    /// <returns>List of sentences</returns>
    IReadOnlyList<string> Segment(string text);
}

using System.Globalization;

namespace PersonaEngine.Lib.ASR.Transcriber;

public class TranscriptSegment
{
    public IReadOnlyDictionary<string, string> Metadata { get; init; }

    /// <summary>
    ///     Gets or sets the text of the segment.
    /// </summary>
    public string Text { get; set; } = string.Empty;

    /// <summary>
    ///     Gets or sets the start time of the segment.
    /// </summary>
    public TimeSpan StartTime { get; set; }

    /// <summary>
    ///     Gets or sets the duration of the segment
    /// </summary>
    public TimeSpan Duration { get; set; }

    /// <summary>
    ///     Gets or sets the confidence of the segment
    /// </summary>
    public float? ConfidenceLevel { get; set; }

    /// <summary>
    ///     Gets or sets the language of the segment.
    /// </summary>
    public CultureInfo? Language { get; set; }

    /// <summary>
    ///     Gets or sets the tokens of the segment.
    /// </summary>
    /// <remarks>
    ///     Not all the transcriptors will provide tokens.
    /// </remarks>
    public IList<TranscriptToken>? Tokens { get; set; }
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering;

/// <summary>
///     カラーブレンディングのモード
/// </summary>
public enum CubismBlendMode
{
    /// <summary>
    ///     通常
    /// </summary>
    Normal = 0,

    /// <summary>
    ///     加算
    /// </summary>
    Additive = 1,

    /// <summary>
    ///     乗算
    /// </summary>
    Multiplicative = 2,

    /// <summary>
    ///     マスク
    /// </summary>
    Mask = 3
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Manages configuration loading, saving, and access
/// </summary>
public interface IUiConfigurationManager
{
    event EventHandler<ConfigurationChangedEventArgs> ConfigurationChanged;

    T GetConfiguration<T>(string sectionKey = null);

    void UpdateConfiguration<T>(T configuration, string? sectionKey = null);

    void SaveConfiguration();

    void ReloadConfiguration();
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Session;
using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Strategies;

public class NoSpeakingBargeInStrategy : IBargeInStrategy
{
    public bool ShouldAllowBargeIn(BargeInContext context) { return context.CurrentState != ConversationState.Speaking; }
}

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
/// Represents an emotion with its timestamp in the audio
/// </summary>
public record EmotionTiming
{
    /// <summary>
    /// Timestamp in seconds when the emotion occurs
    /// </summary>
    public double Timestamp { get; set; }

    /// <summary>
    /// Emotion emoji
    /// </summary>
    public string Emotion { get; set; } = string.Empty;
}

namespace PersonaEngine.Lib.Live2D.Framework;

public static class SDKInfo
{
    public const string Version = "Cubism 5 SDK for Native R1";
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;
using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

public record LlmStreamStartEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp) : IOutputEvent { }

public record LlmChunkEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp, string Chunk) : IOutputEvent { }

public record LlmStreamEndEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp, CompletionReason FinishReason) : IOutputEvent { }

namespace PersonaEngine.Lib.Audio.Player;

public class AudioException(string message, Exception? innerException = null) : Exception(message, innerException);

public class AudioDeviceNotFoundException(string message) : Exception(message);

public class AudioPlayerInitializationException(string message, Exception? innerException = null) : Exception(message, innerException);

namespace PersonaEngine.Lib.Vision;

public interface IVisualQAService : IAsyncDisposable
{
    string? ScreenCaption { get; }

    Task StartAsync(CancellationToken cancellationToken = default);

    Task StopAsync();
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for part-of-speech tagging
/// </summary>
public interface IPosTagger : IDisposable
{
    /// <summary>
    ///     Tags parts of speech in text
    /// </summary>
    Task<IReadOnlyList<PosToken>> TagAsync(string text, CancellationToken cancellationToken = default);
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Interface for configuration section editors
/// </summary>
public interface IConfigSectionEditor
{
    string SectionKey { get; }

    string DisplayName { get; }

    bool HasUnsavedChanges { get; }

    void Initialize();

    void Render();

    void RenderMenuItems();

    void Update(float deltaTime);

    void OnConfigurationChanged(ConfigurationChangedEventArgs args);
}

namespace PersonaEngine.Lib.Live2D.Framework.Model;

/// <summary>
///     Jsonから読み込んだユーザデータを記録しておくための構造体
/// </summary>
public record CubismModelUserDataNode
{
    /// <summary>
    ///     ユーザデータターゲットタイプ
    /// </summary>
    public required string TargetType { get; set; }

    /// <summary>
    ///     ユーザデータターゲットのID
    /// </summary>
    public required string TargetId { get; set; }

    /// <summary>
    ///     ユーザデータ
    /// </summary>
    public required string Value { get; set; }
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Provides notification services for the UI
/// </summary>
public interface INotificationService
{
    void ShowInfo(string message, Action? action = null, string actionLabel = "OK");

    void ShowSuccess(string message, Action? action = null, string actionLabel = "OK");

    void ShowWarning(string message, Action? action = null, string actionLabel = "OK");

    void ShowError(string message, Action? action = null, string actionLabel = "OK");

    IReadOnlyList<Notification> GetActiveNotifications();

    void Update(float deltaTime);
}

using PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Adapters;

public interface IAudioProgressNotifier
{
    event EventHandler<AudioChunkPlaybackStartedEvent>? ChunkPlaybackStarted;

    event EventHandler<AudioChunkPlaybackEndedEvent>? ChunkPlaybackEnded;

    event EventHandler<AudioPlaybackProgressEvent>? PlaybackProgress;

    void RaiseChunkStarted(object? sender, AudioChunkPlaybackStartedEvent args);

    void RaiseChunkEnded(object? sender, AudioChunkPlaybackEndedEvent args);

    void RaiseProgress(object? sender, AudioPlaybackProgressEvent args);
}

using PersonaEngine.Lib.Live2D.Framework.Core;

namespace PersonaEngine.Lib.Live2D.Framework;

/// <summary>
///     ログ出力のレベル
/// </summary>
public enum LogLevel
{
    /// <summary>
    ///     詳細ログ
    /// </summary>
    Verbose = 0,

    /// <summary>
    ///     デバッグログ
    /// </summary>
    Debug,

    /// <summary>
    ///     Infoログ
    /// </summary>
    Info,

    /// <summary>
    ///     警告ログ
    /// </summary>
    Warning,

    /// <summary>
    ///     エラーログ
    /// </summary>
    Error,

    /// <summary>
    ///     ログ出力無効
    /// </summary>
    Off
}

/// <summary>
///     CubismFrameworkに設定するオプション要素を定義するクラス
/// </summary>
public class Option
{
    /// <summary>
    ///     ログ出力の関数ポイ
    /// </summary>
    public required LogFunction LogFunction;

    /// <summary>
    ///     ログ出力レベル設定
    /// </summary>
    public LogLevel LoggingLevel;
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for phoneme lexicon lookup
/// </summary>
public interface ILexicon
{
    public (string? Phonemes, int? Rating) ProcessToken(Token token, TokenContext ctx);
}

using System.Numerics;

using FontStashSharp;

namespace PersonaEngine.Lib.UI.Text.Subtitles;

/// <summary>
///     Interface for defining word animation strategies (scale, color).
/// </summary>
public interface IWordAnimator
{
    Vector2 CalculateScale(float progress);

    FSColor CalculateColor(FSColor startColor, FSColor endColor, float progress);
}

using PersonaEngine.Lib.Live2D.Framework.Rendering;

namespace PersonaEngine.Lib.Live2D.Framework.Model;

/// <summary>
///     テクスチャの色をRGBAで扱うための構造体
/// </summary>
public record PartColorData
{
    public CubismTextureColor Color = new();

    public bool IsOverwritten { get; set; }
}

using PersonaEngine.Lib.Live2D.Framework.Rendering;

namespace PersonaEngine.Lib.Live2D.Framework.Model;

/// <summary>
///     テクスチャの色をRGBAで扱うための構造体
/// </summary>
public record DrawableColorData
{
    public CubismTextureColor Color = new();

    public bool IsOverwritten { get; set; }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for text normalization
/// </summary>
public interface ITextNormalizer
{
    /// <summary>
    ///     Normalizes text for TTS synthesis
    /// </summary>
    /// <param name="text">Input text</param>
    /// <returns>Normalized text</returns>
    string Normalize(string text);
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Session;

public enum ConversationState
{
    Initial,

    Initializing,

    Idle,

    Listening,

    ActiveTurn,

    ProcessingInput,

    WaitingForLlm,

    StreamingResponse,

    Speaking,

    Paused,

    Interrupted,

    Error,

    Ended
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Session;

public interface IConversationSession : IAsyncDisposable
{
    IConversationContext Context { get; }

    Guid SessionId { get; }

    ValueTask RunAsync(CancellationToken cancellationToken);

    ValueTask StopAsync();
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Token with part-of-speech information
/// </summary>
public class PosToken
{
    /// <summary>
    ///     The text of the token
    /// </summary>
    public string Text { get; set; } = string.Empty;

    /// <summary>
    ///     Part of speech tag
    /// </summary>
    public string? PartOfSpeech { get; set; }

    /// <summary>
    ///     Whitespace after this token
    /// </summary>
    public bool IsWhitespace { get; set; }
}

namespace PersonaEngine.Lib.Live2D.Framework.Effect;

public enum EyeState
{
    /// <summary>
    ///     初期状態
    /// </summary>
    First = 0,

    /// <summary>
    ///     まばたきしていない状態
    /// </summary>
    Interval,

    /// <summary>
    ///     まぶたが閉じていく途中の状態
    /// </summary>
    Closing,

    /// <summary>
    ///     まぶたが閉じている状態
    /// </summary>
    Closed,

    /// <summary>
    ///     まぶたが開いていく途中の状態
    /// </summary>
    Opening
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Session;

public enum ConversationTrigger
{
    InitializeRequested,

    InitializeComplete,

    StopRequested,

    PauseRequested,

    ResumeRequested,

    InputDetected,

    InputFinalized,

    LlmRequestSent,

    LlmStreamStarted,

    LlmStreamChunkReceived,

    LlmStreamEnded,

    TtsRequestSent,

    TtsStreamStarted,

    TtsStreamChunkReceived,

    TtsStreamEnded,

    AudioStreamStarted,

    AudioStreamEnded,

    ErrorOccurred,
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

internal record CubismShaderSet
{
    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(Position)
    /// </summary>
    internal int AttributePositionLocation;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(TexCoord)
    /// </summary>
    internal int AttributeTexCoordLocation;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(Texture0)
    /// </summary>
    internal int SamplerTexture0Location;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(Texture1)
    /// </summary>
    internal int SamplerTexture1Location;

    /// <summary>
    ///     シェーダプログラムのアドレス
    /// </summary>
    internal int ShaderProgram;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(BaseColor)
    /// </summary>
    internal int UniformBaseColorLocation;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(ClipMatrix)
    /// </summary>
    internal int UniformClipMatrixLocation;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(Matrix)
    /// </summary>
    internal int UniformMatrixLocation;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(MultiplyColor)
    /// </summary>
    internal int UniformMultiplyColorLocation;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(ScreenColor)
    /// </summary>
    internal int UniformScreenColorLocation;

    /// <summary>
    ///     シェーダプログラムに渡す変数のアドレス(ChannelFlag)
    /// </summary>
    internal int UnifromChannelFlagLocation;
}

namespace PersonaEngine.Lib.Audio;

public interface IMemoryBackedAudioSource
{
    public bool StoresFloats { get; }

    public bool StoresBytes { get; }
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Events;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Events.Output;

public record BaseOutputEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp) : IOutputEvent;

public record ErrorOutputEvent(Guid SessionId, Guid? TurnId, DateTimeOffset Timestamp, Exception Exception) : IOutputEvent { }

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

/// <summary>
///     Stores emotion data extracted from text with its position information
/// </summary>
public record EmotionMarker
{
    /// <summary>
    ///     Character position in the cleaned text
    /// </summary>
    public int Position { get; set; }

    /// <summary>
    ///     Emotion emoji
    /// </summary>
    public string Emotion { get; set; } = string.Empty;
}

namespace PersonaEngine.Lib.TTS.Synthesis;

public class PhonemeContext
{
    public bool UseBritishEnglish { get; set; }

    public bool? NextStartsWithVowel { get; set; }

    public bool HasToToken { get; set; }
}

using OpenAI.Chat;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

public class ChatMessage(Guid messageId, string participantId, string participantName, string text, DateTimeOffset timestamp, bool isPartial, ChatMessageRole role)
{
    public Guid MessageId { get; } = messageId;

    public string ParticipantId { get; } = participantId;

    public string ParticipantName { get; } = participantName;

    public string Text { get; internal set; } = text;

    public DateTimeOffset Timestamp { get; } = timestamp;

    public bool IsPartial { get; internal set; } = isPartial;

    public ChatMessageRole Role { get; } = role;
}

namespace PersonaEngine.Lib.ASR.Transcriber;

public class TranscriptToken
{
    /// <summary>
    ///     The logaritmic confidence of the token.
    /// </summary>
    public float? ConfidenceLog;

    /// <summary>
    ///     The text representation of the token.
    /// </summary>
    public string? Text;

    /// <summary>
    ///     The unique identifier for the token for transcriptors that uses IDs
    /// </summary>
    /// <remarks>
    ///     The type is object to allow for different types of IDs based on the transcriptor, this ID is not used inside
    ///     EchoSharp library.
    /// </remarks>
    public object? Id { get; set; }

    /// <summary>
    ///     The confidence of the token.
    /// </summary>
    public float? Confidence { get; set; }

    /// <summary>
    ///     The confidence of the timestamp of the token.
    /// </summary>
    public float TimestampConfidence { get; set; }

    /// <summary>
    ///     The timestamp confidence sum of the token.
    /// </summary>
    public float TimestampConfidenceSum { get; set; }

    /// <summary>
    ///     The start time of the token.
    /// </summary>
    public TimeSpan StartTime { get; set; }

    /// <summary>
    ///     The duration of the token.
    /// </summary>
    public TimeSpan Duration { get; set; }

    /// <summary>
    ///     Dynamic Time Warping timestamp of the token.
    /// </summary>
    public long DtwTimestamp { get; set; }
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Manages registration and access to configuration sections
/// </summary>
public interface IConfigSectionRegistry
{
    void RegisterSection(IConfigSectionEditor section);

    void UnregisterSection(string sectionKey);

    IConfigSectionEditor GetSection(string sectionKey);

    IReadOnlyList<IConfigSectionEditor> GetSections();
}

namespace PersonaEngine.Lib.Live2D.Framework;

public class CubismCdiJson
{
    public const string Version = "Version";

    public const string Parameters = "Parameters";

    public const string ParameterGroups = "ParameterGroups";

    public const string Parts = "Parts";

    public const string Id = "Id";

    public const string GroupId = "GroupId";

    public const string Name = "Name";
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Events;

public interface IInputEvent : IConversationEvent
{
    string ParticipantId { get; }
}

namespace PersonaEngine.Lib.Configuration;

public record SpoutConfiguration
{
    public required string OutputName { get; init; }

    public required int Width { get; init; }

    public required int Height { get; init; }
}

namespace PersonaEngine.Lib.Live2D.Framework.Model;

/// <summary>
///     テクスチャのカリング設定を管理するための構造体
/// </summary>
public record DrawableCullingData
{
    public bool IsOverwritten { get; set; }

    public bool IsCulling { get; set; }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

/// <summary>
///     Interface for ML-based sentence detection
/// </summary>
public interface IMlSentenceDetector : IDisposable
{
    /// <summary>
    ///     Detects sentences in text
    /// </summary>
    IReadOnlyList<string> Detect(string text);
}

namespace PersonaEngine.Lib.TTS.Profanity;

/// <summary>
///     Represents the severity level of profanity.
/// </summary>
public enum ProfanitySeverity
{
    Clean, // No profanity detected.

    Mild, // Some profanity detected.

    Moderate, // Moderate profanity detected.

    Severe // High level of profanity detected.
}

using OpenAI.Chat;

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Context;

public record ParticipantInfo(
    string          Id,
    string          Name,
    ChatMessageRole Role
);

namespace PersonaEngine.Lib.Live2D.Framework.Effect;

/// <summary>
///     呼吸のパラメータ情報。
/// </summary>
public record BreathParameterData
{
    /// <summary>
    ///     呼吸をひもづけるパラメータIDs
    /// </summary>
    public required string ParameterId { get; set; }

    /// <summary>
    ///     呼吸を正弦波としたときの、波のオフセット
    /// </summary>
    public float Offset { get; set; }

    /// <summary>
    ///     呼吸を正弦波としたときの、波の高さ
    /// </summary>
    public float Peak { get; set; }

    /// <summary>
    ///     呼吸を正弦波としたときの、波の周期
    /// </summary>
    public float Cycle { get; set; }

    /// <summary>
    ///     パラメータへの重み
    /// </summary>
    public float Weight { get; set; }
}

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     画像情報構造体
/// </summary>
public record TextureInfo
{
    /// <summary>
    ///     ファイル名
    /// </summary>
    public required string FileName;

    /// <summary>
    ///     高さ
    /// </summary>
    public int Height;

    /// <summary>
    ///     テクスチャID
    /// </summary>
    public int ID;

    /// <summary>
    ///     横幅
    /// </summary>
    public int Width;
}

namespace PersonaEngine.Lib.Live2D.App;

/// <summary>
///     プラットフォーム依存機能を抽象化する Cubism Platform Abstraction Layer.
///     ファイル読み込みや時刻取得等のプラットフォームに依存する関数をまとめる
/// </summary>
public static class LAppPal
{
    /// <summary>
    ///     デルタ時間（前回フレームとの差分）を取得する
    /// </summary>
    /// <returns>デルタ時間[ms]</returns>
    public static float DeltaTime { get; set; }
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Events;

public interface IOutputEvent : IConversationEvent { }

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Manages UI theme settings
/// </summary>
public interface IUiThemeManager
{
    void ApplyTheme();

    void SetTheme(UiTheme theme);

    UiTheme GetCurrentTheme();
}

using PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Strategies;

public class IgnoreBargeInStrategy : IBargeInStrategy
{
    public bool ShouldAllowBargeIn(BargeInContext context) { return false; }
}

namespace PersonaEngine.Lib.UI.GUI;

/// <summary>
///     Exception thrown when saving configuration fails
/// </summary>
public class ConfigurationSaveException : Exception
{
    public ConfigurationSaveException(string message) : base(message) { }

    public ConfigurationSaveException(string message, Exception innerException) : base(message, innerException) { }
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering.OpenGL;

public enum ShaderNames
{
    // SetupMask
    SetupMask,

    //Normal
    Normal,

    NormalMasked,

    NormalMaskedInverted,

    NormalPremultipliedAlpha,

    NormalMaskedPremultipliedAlpha,

    NormalMaskedInvertedPremultipliedAlpha,

    //Add
    Add,

    AddMasked,

    AddMaskedInverted,

    AddPremultipliedAlpha,

    AddMaskedPremultipliedAlpha,

    AddMaskedPremultipliedAlphaInverted,

    //Mult
    Mult,

    MultMasked,

    MultMaskedInverted,

    MultPremultipliedAlpha,

    MultMaskedPremultipliedAlpha,

    MultMaskedPremultipliedAlphaInverted
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Events;

public interface IConversationEvent
{
    Guid SessionId { get; }

    Guid? TurnId { get; }

    DateTimeOffset Timestamp { get; }
}

using Silk.NET.OpenGL;

namespace PersonaEngine.Lib.UI.Common;

public interface IStartupTask
{
    void Execute(GL gl);
}

namespace PersonaEngine.Lib.Live2D.Behaviour.Emotion;

internal record EmotionMarkerInfo
{
    public required string MarkerId { get; init; }

    public required string Emotion { get; init; }
}

namespace PersonaEngine.Lib.TTS.Synthesis;

public record SimplePhonemeEntry(string Phoneme) : PhonemeEntry;

namespace PersonaEngine.Lib.UI.RouletteWheel;

public delegate void WheelStoppedEventHandler(string selectedOption);

namespace PersonaEngine.Lib.Audio.Player;

public enum PlayerState
{
    Uninitialized,

    Initialized,

    Starting,

    Playing,

    Stopping,

    Stopped,

    Error
}

namespace PersonaEngine.Lib.Live2D.Framework.Rendering;

public enum RenderType
{
    OpenGL
}

namespace PersonaEngine.Lib.Core.Conversation.Abstractions.Strategies;

public enum BargeInType
{
    Ignore,

    Allow,

    NoSpeaking,

    MinWords,

    MinWordsNoSpeaking
}

namespace PersonaEngine.Lib.Core.Conversation.Implementations.Events.Common;

public enum CompletionReason
{
    Completed,

    Cancelled,

    Error
}

protected virtual void Dispose (bool disposing) {}

This code segment functions to: Manage resource cleanup operations during object disposal by conditionally releasing managed components and tracking disposal state to prevent redundant termination attempts.

```c_sharp
[TASK_BEGIN]
protected virtual void Dispose(bool disposing)
{
    if (_disposed)
    {
        return;
    }

    if (disposing)
    {
        // Dispose managed resources
        _initLock.Dispose();
        _optionsChangeRegistration?.Dispose();
    }

    // Free unmanaged resources and perform other cleanup
    DisposeResources();

    _disposed = true;
}
[TASK_END]
```

protected virtual void Dispose(bool disposing)
    {
        if ( _disposed )
        {
            return;
        }
        if ( disposing )
        {
            _optionsChangeRegistration?.Dispose();
            DisposeResources();
        }
        _disposed = true;
    }

protected virtual void Dispose(bool disposing)
{
    if (_disposed)
    {
        return;
    }
    if (disposing)
    {
        _initLock.Dispose();
        _optionsChangeRegistration?.Dispose();
    }
    DisposeResources();
    _disposed = true;
}