//
// Copyright (c) 2019-2021 Ryujinx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//

using Ryujinx.Audio.Common;
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.Command;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Server.Effect;
using Ryujinx.Audio.Renderer.Server.Mix;
using Ryujinx.Audio.Renderer.Server.Performance;
using Ryujinx.Audio.Renderer.Server.Sink;
using Ryujinx.Audio.Renderer.Server.Splitter;
using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils;
using System;
using System.Diagnostics;

namespace Ryujinx.Audio.Renderer.Server
{
    public class CommandGenerator
    {
        private CommandBuffer _commandBuffer;
        private RendererSystemContext _rendererContext;
        private VoiceContext _voiceContext;
        private MixContext _mixContext;
        private EffectContext _effectContext;
        private SinkContext _sinkContext;
        private SplitterContext _splitterContext;
        private PerformanceManager _performanceManager;

        public CommandGenerator(CommandBuffer commandBuffer, RendererSystemContext rendererContext, VoiceContext voiceContext, MixContext mixContext, EffectContext effectContext, SinkContext sinkContext, SplitterContext splitterContext, PerformanceManager performanceManager)
        {
            _commandBuffer = commandBuffer;
            _rendererContext = rendererContext;
            _voiceContext = voiceContext;
            _mixContext = mixContext;
            _effectContext = effectContext;
            _sinkContext = sinkContext;
            _splitterContext = splitterContext;
            _performanceManager = performanceManager;

            _commandBuffer.GenerateClearMixBuffer(Constants.InvalidNodeId);
        }

        private void GenerateDataSource(ref VoiceState voiceState, Memory<VoiceUpdateState> dspState, int channelIndex)
        {
            if (voiceState.MixId != Constants.UnusedMixId)
            {
                ref MixState mix = ref _mixContext.GetState(voiceState.MixId);

                _commandBuffer.GenerateDepopPrepare(dspState,
                                                    _rendererContext.DepopBuffer,
                                                    mix.BufferCount,
                                                    mix.BufferOffset,
                                                    voiceState.NodeId,
                                                    voiceState.WasPlaying);
            }
            else if (voiceState.SplitterId != Constants.UnusedSplitterId)
            {
                int destinationId = 0;

                while (true)
                {
                    Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);

                    if (destinationSpan.IsEmpty)
                    {
                        break;
                    }

                    ref SplitterDestination destination = ref destinationSpan[0];

                    if (destination.IsConfigured())
                    {
                        int mixId = destination.DestinationId;

                        if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt)
                        {
                            ref MixState mix = ref _mixContext.GetState(mixId);

                            _commandBuffer.GenerateDepopPrepare(dspState,
                                                                _rendererContext.DepopBuffer,
                                                                mix.BufferCount,
                                                                mix.BufferOffset,
                                                                voiceState.NodeId,
                                                                voiceState.WasPlaying);

                            destination.MarkAsNeedToUpdateInternalState();
                        }
                    }
                }
            }

            if (!voiceState.WasPlaying)
            {
                Debug.Assert(voiceState.SampleFormat != SampleFormat.Adpcm || channelIndex == 0);

                if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported())
                {
                    _commandBuffer.GenerateDataSourceVersion2(ref voiceState,
                                                              dspState,
                                                              (ushort)_rendererContext.MixBufferCount,
                                                              (ushort)channelIndex,
                                                              voiceState.NodeId);
                }
                else
                {
                    switch (voiceState.SampleFormat)
                    {
                        case SampleFormat.PcmInt16:
                            _commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState,
                                                                              dspState,
                                                                              (ushort)_rendererContext.MixBufferCount,
                                                                              (ushort)channelIndex,
                                                                              voiceState.NodeId);
                            break;
                        case SampleFormat.PcmFloat:
                            _commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState,
                                                                              dspState,
                                                                              (ushort)_rendererContext.MixBufferCount,
                                                                              (ushort)channelIndex,
                                                                              voiceState.NodeId);
                            break;
                        case SampleFormat.Adpcm:
                            _commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState,
                                                                           dspState,
                                                                           (ushort)_rendererContext.MixBufferCount,
                                                                           voiceState.NodeId);
                            break;
                        default:
                            throw new NotImplementedException($"Unsupported data source {voiceState.SampleFormat}");
                    }
                }
            }
        }

        private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory<VoiceUpdateState> state, int baseIndex, int bufferOffset, int nodeId)
        {
            bool supportsOptimizedPath = _rendererContext.BehaviourContext.IsBiquadFilterGroupedOptimizationSupported();

            if (supportsOptimizedPath && voiceState.BiquadFilters[0].Enable && voiceState.BiquadFilters[1].Enable)
            {
                Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state).Slice(VoiceUpdateState.BiquadStateOffset, VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount);
                Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory);

                _commandBuffer.GenerateGroupedBiquadFilter(baseIndex, voiceState.BiquadFilters.ToSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
            }
            else
            {
                for (int i = 0; i < voiceState.BiquadFilters.Length; i++)
                {
                    ref BiquadFilterParameter filter = ref voiceState.BiquadFilters[i];

                    if (filter.Enable)
                    {
                        Memory<byte> biquadStateRawMemory = SpanMemoryManager<byte>.Cast(state).Slice(VoiceUpdateState.BiquadStateOffset, VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount);

                        Memory<BiquadFilterState> stateMemory = SpanMemoryManager<BiquadFilterState>.Cast(biquadStateRawMemory);

                        _commandBuffer.GenerateBiquadFilter(baseIndex,
                                                            ref filter,
                                                            stateMemory.Slice(i, 1),
                                                            bufferOffset,
                                                            bufferOffset,
                                                            !voiceState.BiquadFilterNeedInitialization[i],
                                                            nodeId);
                    }
                }
            }
        }

        private void GenerateVoiceMix(Span<float> mixVolumes, Span<float> previousMixVolumes, Memory<VoiceUpdateState> state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId)
        {
            if (bufferCount > Constants.VoiceChannelCountMax)
            {
                _commandBuffer.GenerateMixRampGrouped(bufferCount,
                                                      bufferIndex,
                                                      bufferOffset,
                                                      previousMixVolumes,
                                                      mixVolumes,
                                                      state,
                                                      nodeId);
            }
            else
            {
                for (int i = 0; i < bufferCount; i++)
                {
                    float previousMixVolume = previousMixVolumes[i];
                    float mixVolume = mixVolumes[i];

                    if (mixVolume != 0.0f || previousMixVolume != 0.0f)
                    {
                        _commandBuffer.GenerateMixRamp(previousMixVolume,
                                                       mixVolume,
                                                       bufferIndex,
                                                       bufferOffset + (uint)i,
                                                       i,
                                                       state,
                                                       nodeId);
                    }
                }
            }
        }

        private void GenerateVoice(ref VoiceState voiceState)
        {
            int nodeId = voiceState.NodeId;
            uint channelsCount = voiceState.ChannelsCount;

            for (int channelIndex = 0; channelIndex < channelsCount; channelIndex++)
            {
                Memory<VoiceUpdateState> dspStateMemory = _voiceContext.GetUpdateStateForDsp(voiceState.ChannelResourceIds[channelIndex]);

                ref VoiceChannelResource channelResource = ref _voiceContext.GetChannelResource(voiceState.ChannelResourceIds[channelIndex]);

                PerformanceDetailType dataSourceDetailType = PerformanceDetailType.Adpcm;

                if (voiceState.SampleFormat == SampleFormat.PcmInt16)
                {
                    dataSourceDetailType = PerformanceDetailType.PcmInt16;
                }
                else if (voiceState.SampleFormat == SampleFormat.PcmFloat)
                {
                    dataSourceDetailType = PerformanceDetailType.PcmFloat;
                }

                bool performanceInitialized = false;

                PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

                if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, dataSourceDetailType, PerformanceEntryType.Voice, nodeId))
                {
                    performanceInitialized = true;

                    GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
                }

                GenerateDataSource(ref voiceState, dspStateMemory, channelIndex);

                if (performanceInitialized)
                {
                    GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
                }

                if (voiceState.WasPlaying)
                {
                    voiceState.PreviousVolume = 0.0f;
                }
                else if (voiceState.HasAnyDestination())
                {
                    performanceInitialized = false;

                    if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.BiquadFilter, PerformanceEntryType.Voice, nodeId))
                    {
                        performanceInitialized = true;

                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
                    }

                    GenerateBiquadFilterForVoice(ref voiceState, dspStateMemory, (int)_rendererContext.MixBufferCount, channelIndex, nodeId);

                    if (performanceInitialized)
                    {
                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
                    }

                    performanceInitialized = false;

                    if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.VolumeRamp, PerformanceEntryType.Voice, nodeId))
                    {
                        performanceInitialized = true;

                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
                    }

                    _commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume,
                                                      voiceState.Volume,
                                                      _rendererContext.MixBufferCount + (uint)channelIndex,
                                                      nodeId);

                    if (performanceInitialized)
                    {
                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
                    }

                    voiceState.PreviousVolume = voiceState.Volume;

                    if (voiceState.MixId == Constants.UnusedMixId)
                    {
                        if (voiceState.SplitterId != Constants.UnusedSplitterId)
                        {
                            int destinationId = channelIndex;

                            while (true)
                            {
                                Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);

                                if (destinationSpan.IsEmpty)
                                {
                                    break;
                                }

                                ref SplitterDestination destination = ref destinationSpan[0];

                                destinationId += (int)channelsCount;

                                if (destination.IsConfigured())
                                {
                                    int mixId = destination.DestinationId;

                                    if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt)
                                    {
                                        ref MixState mix = ref _mixContext.GetState(mixId);

                                        GenerateVoiceMix(destination.MixBufferVolume,
                                                         destination.PreviousMixBufferVolume,
                                                         dspStateMemory,
                                                         mix.BufferOffset,
                                                         mix.BufferCount,
                                                         _rendererContext.MixBufferCount + (uint)channelIndex,
                                                         nodeId);

                                        destination.MarkAsNeedToUpdateInternalState();
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        ref MixState mix = ref _mixContext.GetState(voiceState.MixId);

                        performanceInitialized = false;

                        if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.Voice, nodeId))
                        {
                            performanceInitialized = true;

                            GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
                        }

                        GenerateVoiceMix(channelResource.Mix.ToSpan(),
                                         channelResource.PreviousMix.ToSpan(),
                                         dspStateMemory,
                                         mix.BufferOffset,
                                         mix.BufferCount,
                                         _rendererContext.MixBufferCount + (uint)channelIndex,
                                         nodeId);

                        if (performanceInitialized)
                        {
                            GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
                        }

                        channelResource.UpdateState();
                    }

                    for (int i = 0; i < voiceState.BiquadFilterNeedInitialization.Length; i++)
                    {
                        voiceState.BiquadFilterNeedInitialization[i] = voiceState.BiquadFilters[i].Enable;
                    }
                }
            }
        }

        public void GenerateVoices()
        {
            for (int i = 0; i < _voiceContext.GetCount(); i++)
            {
                ref VoiceState sortedState = ref _voiceContext.GetSortedState(i);

                if (!sortedState.ShouldSkip() && sortedState.UpdateForCommandGeneration(_voiceContext))
                {
                    int nodeId = sortedState.NodeId;

                    PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

                    bool performanceInitialized = false;

                    if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.Voice, nodeId))
                    {
                        performanceInitialized = true;

                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
                    }

                    GenerateVoice(ref sortedState);

                    if (performanceInitialized)
                    {
                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
                    }
                }
            }

            _splitterContext.UpdateInternalState();
        }

        public void GeneratePerformance(ref PerformanceEntryAddresses performanceEntryAddresses, PerformanceCommand.Type type, int nodeId)
        {
            _commandBuffer.GeneratePerformance(ref performanceEntryAddresses, type, nodeId);
        }

        private void GenerateBufferMixerEffect(int bufferOffset, BufferMixEffect effect, int nodeId)
        {
            Debug.Assert(effect.Type == EffectType.BufferMix);

            if (effect.IsEnabled)
            {
                for (int i = 0; i < effect.Parameter.MixesCount; i++)
                {
                    if (effect.Parameter.Volumes[i] != 0.0f)
                    {
                        _commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i],
                                                   (uint)bufferOffset + effect.Parameter.Output[i],
                                                   nodeId,
                                                   effect.Parameter.Volumes[i]);
                    }
                }
            }
        }

        private void GenerateAuxEffect(uint bufferOffset, AuxiliaryBufferEffect effect, int nodeId)
        {
            Debug.Assert(effect.Type == EffectType.AuxiliaryBuffer);

            if (effect.IsEnabled)
            {
                effect.GetWorkBuffer(0);
                effect.GetWorkBuffer(1);
            }

            if (effect.State.SendBufferInfoBase != 0 && effect.State.ReturnBufferInfoBase != 0)
            {
                int i = 0;
                uint writeOffset = 0;
                for (uint channelIndex = effect.Parameter.ChannelCount; channelIndex != 0; channelIndex--)
                {
                    uint newUpdateCount = writeOffset + _commandBuffer.CommandList.SampleCount;

                    uint updateCount;

                    if (channelIndex != 1)
                    {
                        updateCount = 0;
                    }
                    else
                    {
                        updateCount = newUpdateCount;
                    }

                    _commandBuffer.GenerateAuxEffect(bufferOffset,
                                                     effect.Parameter.Input[i],
                                                     effect.Parameter.Output[i],
                                                     ref effect.State,
                                                     effect.IsEnabled,
                                                     effect.Parameter.BufferStorageSize,
                                                     effect.State.SendBufferInfoBase,
                                                     effect.State.ReturnBufferInfoBase,
                                                     updateCount,
                                                     writeOffset,
                                                     nodeId);

                    writeOffset = newUpdateCount;

                    i++;
                }
            }
        }

        private void GenerateDelayEffect(uint bufferOffset, DelayEffect effect, int nodeId)
        {
            Debug.Assert(effect.Type == EffectType.Delay);

            ulong workBuffer = effect.GetWorkBuffer(-1);

            _commandBuffer.GenerateDelayEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId);
        }

        private void GenerateReverbEffect(uint bufferOffset, ReverbEffect effect, int nodeId, bool isLongSizePreDelaySupported)
        {
            Debug.Assert(effect.Type == EffectType.Reverb);

            ulong workBuffer = effect.GetWorkBuffer(-1);

            _commandBuffer.GenerateReverbEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId, isLongSizePreDelaySupported);
        }

        private void GenerateReverb3dEffect(uint bufferOffset, Reverb3dEffect effect, int nodeId)
        {
            Debug.Assert(effect.Type == EffectType.Reverb3d);

            ulong workBuffer = effect.GetWorkBuffer(-1);

            _commandBuffer.GenerateReverb3dEffect(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId);
        }

        private void GenerateBiquadFilterEffect(uint bufferOffset, BiquadFilterEffect effect, int nodeId)
        {
            Debug.Assert(effect.Type == EffectType.BiquadFilter);

            if (effect.IsEnabled)
            {
                bool needInitialization = effect.Parameter.Status == UsageState.Invalid ||
                                         (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed());

                BiquadFilterParameter parameter = new BiquadFilterParameter();

                parameter.Enable = true;
                effect.Parameter.Denominator.ToSpan().CopyTo(parameter.Denominator.ToSpan());
                effect.Parameter.Numerator.ToSpan().CopyTo(parameter.Numerator.ToSpan());

                for (int i = 0; i < effect.Parameter.ChannelCount; i++)
                {
                    _commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1),
                                                        effect.Parameter.Input[i],
                                                        effect.Parameter.Output[i],
                                                        needInitialization,
                                                        nodeId);
                }
            }
            else
            {
                for (int i = 0; i < effect.Parameter.ChannelCount; i++)
                {
                    uint inputBufferIndex = bufferOffset + effect.Parameter.Input[i];
                    uint outputBufferIndex = bufferOffset + effect.Parameter.Output[i];

                    // If the input and output isn't the same, generate a command.
                    if (inputBufferIndex != outputBufferIndex)
                    {
                        _commandBuffer.GenerateCopyMixBuffer(inputBufferIndex, outputBufferIndex, nodeId);
                    }
                }
            }
        }

        private void GenerateLimiterEffect(uint bufferOffset, LimiterEffect effect, int nodeId, int effectId)
        {
            Debug.Assert(effect.Type == EffectType.Limiter);

            ulong workBuffer = effect.GetWorkBuffer(-1);

            if (_rendererContext.BehaviourContext.IsEffectInfoVersion2Supported())
            {
                Memory<EffectResultState> dspResultState = _effectContext.GetDspStateMemory(effectId);

                _commandBuffer.GenerateLimiterEffectVersion2(bufferOffset, effect.Parameter, effect.State, dspResultState, effect.IsEnabled, workBuffer, nodeId);
            }
            else
            {
                _commandBuffer.GenerateLimiterEffectVersion1(bufferOffset, effect.Parameter, effect.State, effect.IsEnabled, workBuffer, nodeId);
            }
        }

        private void GenerateCaptureEffect(uint bufferOffset, CaptureBufferEffect effect, int nodeId)
        {
            Debug.Assert(effect.Type == EffectType.CaptureBuffer);

            if (effect.IsEnabled)
            {
                effect.GetWorkBuffer(0);
            }

            if (effect.State.SendBufferInfoBase != 0)
            {
                int i = 0;
                uint writeOffset = 0;

                for (uint channelIndex = effect.Parameter.ChannelCount; channelIndex != 0; channelIndex--)
                {
                    uint newUpdateCount = writeOffset + _commandBuffer.CommandList.SampleCount;

                    uint updateCount;

                    if (channelIndex != 1)
                    {
                        updateCount = 0;
                    }
                    else
                    {
                        updateCount = newUpdateCount;
                    }

                    _commandBuffer.GenerateCaptureEffect(bufferOffset,
                                                         effect.Parameter.Input[i],
                                                         effect.State.SendBufferInfo,
                                                         effect.IsEnabled,
                                                         effect.Parameter.BufferStorageSize,
                                                         effect.State.SendBufferInfoBase,
                                                         updateCount,
                                                         writeOffset,
                                                         nodeId);

                    writeOffset = newUpdateCount;

                    i++;
                }
            }
        }

        private void GenerateEffect(ref MixState mix, int effectId, BaseEffect effect)
        {
            int nodeId = mix.NodeId;

            bool isFinalMix = mix.MixId == Constants.FinalMixId;

            PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

            bool performanceInitialized = false;

            if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, effect.GetPerformanceDetailType(),
                                                isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, nodeId))
            {
                performanceInitialized = true;

                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
            }

            switch (effect.Type)
            {
                case EffectType.BufferMix:
                    GenerateBufferMixerEffect((int)mix.BufferOffset, (BufferMixEffect)effect, nodeId);
                    break;
                case EffectType.AuxiliaryBuffer:
                    GenerateAuxEffect(mix.BufferOffset, (AuxiliaryBufferEffect)effect, nodeId);
                    break;
                case EffectType.Delay:
                    GenerateDelayEffect(mix.BufferOffset, (DelayEffect)effect, nodeId);
                    break;
                case EffectType.Reverb:
                    GenerateReverbEffect(mix.BufferOffset, (ReverbEffect)effect, nodeId, mix.IsLongSizePreDelaySupported);
                    break;
                case EffectType.Reverb3d:
                    GenerateReverb3dEffect(mix.BufferOffset, (Reverb3dEffect)effect, nodeId);
                    break;
                case EffectType.BiquadFilter:
                    GenerateBiquadFilterEffect(mix.BufferOffset, (BiquadFilterEffect)effect, nodeId);
                    break;
                case EffectType.Limiter:
                    GenerateLimiterEffect(mix.BufferOffset, (LimiterEffect)effect, nodeId, effectId);
                    break;
                case EffectType.CaptureBuffer:
                    GenerateCaptureEffect(mix.BufferOffset, (CaptureBufferEffect)effect, nodeId);
                    break;
                default:
                    throw new NotImplementedException($"Unsupported effect type {effect.Type}");
            }

            if (performanceInitialized)
            {
                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
            }

            effect.UpdateForCommandGeneration();
        }

        private void GenerateEffects(ref MixState mix)
        {
            ReadOnlySpan<int> effectProcessingOrderArray = mix.EffectProcessingOrderArray;

            Debug.Assert(_effectContext.GetCount() == 0 || !effectProcessingOrderArray.IsEmpty);

            for (int i = 0; i < _effectContext.GetCount(); i++)
            {
                int effectOrder = effectProcessingOrderArray[i];

                if (effectOrder == Constants.InvalidProcessingOrder)
                {
                    break;
                }

                // BaseEffect is a class, we don't need to pass it by ref
                BaseEffect effect = _effectContext.GetEffect(effectOrder);

                Debug.Assert(effect.Type != EffectType.Invalid);
                Debug.Assert(effect.MixId == mix.MixId);

                if (!effect.ShouldSkip())
                {
                    GenerateEffect(ref mix, effectOrder, effect);
                }
            }
        }

        private void GenerateMix(ref MixState mix)
        {
            if (mix.HasAnyDestination())
            {
                Debug.Assert(mix.DestinationMixId != Constants.UnusedMixId || mix.DestinationSplitterId != Constants.UnusedSplitterId);

                if (mix.DestinationMixId == Constants.UnusedMixId)
                {
                    if (mix.DestinationSplitterId != Constants.UnusedSplitterId)
                    {
                        int destinationId = 0;

                        while (true)
                        {
                            int destinationIndex = destinationId++;

                            Span<SplitterDestination> destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);

                            if (destinationSpan.IsEmpty)
                            {
                                break;
                            }

                            ref SplitterDestination destination = ref destinationSpan[0];

                            if (destination.IsConfigured())
                            {
                                int mixId = destination.DestinationId;

                                if (mixId < _mixContext.GetCount() && mixId != Constants.UnusedSplitterIdInt)
                                {
                                    ref MixState destinationMix = ref _mixContext.GetState(mixId);

                                    uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount);

                                    for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++)
                                    {
                                        float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex);

                                        if (volume != 0.0f)
                                        {
                                            _commandBuffer.GenerateMix(inputBufferIndex,
                                                                       destinationMix.BufferOffset + bufferDestinationIndex,
                                                                       mix.NodeId,
                                                                       volume);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                else
                {
                    ref MixState destinationMix = ref _mixContext.GetState(mix.DestinationMixId);

                    for (uint bufferIndex = 0; bufferIndex < mix.BufferCount; bufferIndex++)
                    {
                        for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++)
                        {
                            float volume = mix.Volume * mix.GetMixBufferVolume((int)bufferIndex, (int)bufferDestinationIndex);

                            if (volume != 0.0f)
                            {
                                _commandBuffer.GenerateMix(mix.BufferOffset + bufferIndex,
                                                           destinationMix.BufferOffset + bufferDestinationIndex,
                                                           mix.NodeId,
                                                           volume);
                            }
                        }
                    }
                }
            }
        }

        private void GenerateSubMix(ref MixState subMix)
        {
            _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer,
                                                             subMix.BufferOffset,
                                                             subMix.BufferCount,
                                                             subMix.NodeId,
                                                             subMix.SampleRate);

            GenerateEffects(ref subMix);

            PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

            int nodeId = subMix.NodeId;

            bool performanceInitialized = false;

            if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.SubMix, nodeId))
            {
                performanceInitialized = true;

                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
            }

            GenerateMix(ref subMix);

            if (performanceInitialized)
            {
                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
            }
        }

        public void GenerateSubMixes()
        {
            for (int id = 0; id < _mixContext.GetCount(); id++)
            {
                ref MixState sortedState = ref _mixContext.GetSortedState(id);

                if (sortedState.IsUsed && sortedState.MixId != Constants.FinalMixId)
                {
                    int nodeId = sortedState.NodeId;

                    PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

                    bool performanceInitialized = false;

                    if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.SubMix, nodeId))
                    {
                        performanceInitialized = true;

                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
                    }

                    GenerateSubMix(ref sortedState);

                    if (performanceInitialized)
                    {
                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
                    }
                }
            }
        }

        private void GenerateFinalMix()
        {
            ref MixState finalMix = ref _mixContext.GetFinalState();

            _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer,
                                                             finalMix.BufferOffset,
                                                             finalMix.BufferCount,
                                                             finalMix.NodeId,
                                                             finalMix.SampleRate);

            GenerateEffects(ref finalMix);

            PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

            int nodeId = finalMix.NodeId;

            bool performanceInitialized = false;

            if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.Mix, PerformanceEntryType.FinalMix, nodeId))
            {
                performanceInitialized = true;

                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
            }

            // Only generate volume command if the volume isn't 100%.
            if (finalMix.Volume != 1.0f)
            {
                for (uint bufferIndex = 0; bufferIndex < finalMix.BufferCount; bufferIndex++)
                {
                    bool performanceSubInitialized = false;

                    if (_performanceManager != null && _performanceManager.IsTargetNodeId(nodeId) && _performanceManager.GetNextEntry(out performanceEntry, PerformanceDetailType.VolumeRamp, PerformanceEntryType.FinalMix, nodeId))
                    {
                        performanceSubInitialized = true;

                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
                    }

                    _commandBuffer.GenerateVolume(finalMix.Volume,
                                                  finalMix.BufferOffset + bufferIndex,
                                                  nodeId);

                    if (performanceSubInitialized)
                    {
                        GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
                    }
                }
            }

            if (performanceInitialized)
            {
                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
            }
        }

        public void GenerateFinalMixes()
        {
            int nodeId = _mixContext.GetFinalState().NodeId;

            PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

            bool performanceInitialized = false;

            if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.FinalMix, nodeId))
            {
                performanceInitialized = true;

                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
            }

            GenerateFinalMix();

            if (performanceInitialized)
            {
                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, nodeId);
            }
        }

        private void GenerateCircularBuffer(CircularBufferSink sink, ref MixState finalMix)
        {
            _commandBuffer.GenerateCircularBuffer(finalMix.BufferOffset, sink, Constants.InvalidNodeId);
        }

        private void GenerateDevice(DeviceSink sink, ref MixState finalMix)
        {
            if (_commandBuffer.CommandList.SampleRate != 48000 && sink.UpsamplerState == null)
            {
                sink.UpsamplerState = _rendererContext.UpsamplerManager.Allocate();
            }

            bool useCustomDownMixingCommand = _rendererContext.ChannelCount == 2 && sink.Parameter.DownMixParameterEnabled;

            if (useCustomDownMixingCommand)
            {
                _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
                                                               sink.Parameter.Input.ToSpan(),
                                                               sink.Parameter.Input.ToSpan(),
                                                               sink.DownMixCoefficients,
                                                               Constants.InvalidNodeId);
            }
            // NOTE: We do the downmixing at the DSP level as it's easier that way.
            else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6)
            {
                _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
                                                               sink.Parameter.Input.ToSpan(),
                                                               sink.Parameter.Input.ToSpan(),
                                                               Constants.DefaultSurroundToStereoCoefficients,
                                                               Constants.InvalidNodeId);
            }

            CommandList commandList = _commandBuffer.CommandList;

            if (sink.UpsamplerState != null)
            {
                _commandBuffer.GenerateUpsample(finalMix.BufferOffset,
                                                sink.UpsamplerState,
                                                sink.Parameter.InputCount,
                                                sink.Parameter.Input.ToSpan(),
                                                commandList.BufferCount,
                                                commandList.SampleCount,
                                                commandList.SampleRate,
                                                Constants.InvalidNodeId);
            }

            _commandBuffer.GenerateDeviceSink(finalMix.BufferOffset,
                                              sink,
                                              _rendererContext.SessionId,
                                              commandList.Buffers,
                                              Constants.InvalidNodeId);
        }

        private void GenerateSink(BaseSink sink, ref MixState finalMix)
        {
            bool performanceInitialized = false;

            PerformanceEntryAddresses performanceEntry = new PerformanceEntryAddresses();

            if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, PerformanceEntryType.Sink, sink.NodeId))
            {
                performanceInitialized = true;

                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, sink.NodeId);
            }

            if (!sink.ShouldSkip)
            {
                switch (sink.Type)
                {
                    case SinkType.CircularBuffer:
                        GenerateCircularBuffer((CircularBufferSink)sink, ref finalMix);
                        break;
                    case SinkType.Device:
                        GenerateDevice((DeviceSink)sink, ref finalMix);
                        break;
                    default:
                        throw new NotImplementedException($"Unsupported sink type {sink.Type}");
                }

                sink.UpdateForCommandGeneration();
            }

            if (performanceInitialized)
            {
                GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.End, sink.NodeId);
            }
        }

        public void GenerateSinks()
        {
            ref MixState finalMix = ref _mixContext.GetFinalState();

            for (int i = 0; i < _sinkContext.GetCount(); i++)
            {
                // BaseSink is a class, we don't need to pass it by ref
                BaseSink sink = _sinkContext.GetSink(i);

                if (sink.IsUsed)
                {
                    GenerateSink(sink, ref finalMix);
                }
            }
        }
    }
}