using Ryujinx.Audio.Renderer.Dsp.Effect;
using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Parameter.Effect;
using Ryujinx.Audio.Renderer.Server.Effect;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Ryujinx.Audio.Renderer.Dsp.Command
{
    public class CompressorCommand : ICommand
    {
        private const int FixedPointPrecision = 15;

        public bool Enabled { get; set; }

        public int NodeId { get; private set; }

        public CommandType CommandType => CommandType.Compressor;

        public uint EstimatedProcessingTime { get; set; }

        public CompressorParameter Parameter => _parameter;
        public Memory<CompressorState> State { get; private set; }
        public Memory<EffectResultState> ResultState { get; private set; }
        public ushort[] OutputBufferIndices { get; }
        public ushort[] InputBufferIndices { get; }
        public bool IsEffectEnabled { get; private set; }

        private CompressorParameter _parameter;

        public CompressorCommand()
        {
            InputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
            OutputBufferIndices = new ushort[Constants.VoiceChannelCountMax];
        }

        public CompressorCommand Initialize(uint bufferOffset, CompressorParameter parameter, Memory<CompressorState> state, Memory<EffectResultState> resultState, bool isEnabled, int nodeId)
        {
            Enabled = true;
            NodeId = nodeId;
            _parameter = parameter;
            State = state;
            ResultState = resultState;

            IsEffectEnabled = isEnabled;
            
            Span<byte> inputSpan = _parameter.Input.AsSpan();
            Span<byte> outputSpan = _parameter.Output.AsSpan();

            for (int i = 0; i < _parameter.ChannelCount; i++)
            {
                InputBufferIndices[i] = (ushort)(bufferOffset + inputSpan[i]);
                OutputBufferIndices[i] = (ushort)(bufferOffset + outputSpan[i]);
            }

            return this;
        }

        public void Process(CommandList context)
        {
            ref CompressorState state = ref State.Span[0];

            if (IsEffectEnabled)
            {
                if (_parameter.Status == UsageState.Invalid)
                {
                    state = new CompressorState(ref _parameter);
                }
                else if (_parameter.Status == UsageState.New)
                {
                    state.UpdateParameter(ref _parameter);
                }
            }

            ProcessCompressor(context, ref state);
        }

        private unsafe void ProcessCompressor(CommandList context, ref CompressorState state)
        {
            Debug.Assert(_parameter.IsChannelCountValid());

            if (IsEffectEnabled && _parameter.IsChannelCountValid())
            {
                if (!ResultState.IsEmpty && _parameter.StatisticsReset)
                {
                    ref CompressorStatistics statistics = ref MemoryMarshal.Cast<byte, CompressorStatistics>(ResultState.Span[0].SpecificData)[0];

                    statistics.Reset(_parameter.ChannelCount);
                }

                Span<nint> inputBuffers = stackalloc nint[_parameter.ChannelCount];
                Span<nint> outputBuffers = stackalloc nint[_parameter.ChannelCount];
                Span<float> channelInput = stackalloc float[_parameter.ChannelCount];
                ExponentialMovingAverage inputMovingAverage = state.InputMovingAverage;
                float unknown4 = state.Unknown4;
                ExponentialMovingAverage compressionGainAverage = state.CompressionGainAverage;
                float previousCompressionEmaAlpha = state.PreviousCompressionEmaAlpha;

                for (int i = 0; i < _parameter.ChannelCount; i++)
                {
                    inputBuffers[i] = context.GetBufferPointer(InputBufferIndices[i]);
                    outputBuffers[i] = context.GetBufferPointer(OutputBufferIndices[i]);
                }

                for (int sampleIndex = 0; sampleIndex < context.SampleCount; sampleIndex++)
                {
                    for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
                    {
                        channelInput[channelIndex] = *((float*)inputBuffers[channelIndex] + sampleIndex);
                    }

                    float mean = FloatingPointHelper.MeanSquare(channelInput);
                    float newMean = inputMovingAverage.Update(mean, _parameter.InputGain);
                    float y = FloatingPointHelper.Log10(newMean) * 10.0f;
                    float z = 1.0f;

                    bool unknown10OutOfRange = y >= state.Unknown10;

                    if (newMean < 1.0e-10f)
                    {
                        y = -100.0f;

                        unknown10OutOfRange = state.Unknown10 <= -100.0f;
                    }

                    if (unknown10OutOfRange)
                    {
                        float tmpGain;

                        if (y >= state.Unknown14)
                        {
                            tmpGain = ((1.0f / _parameter.Ratio) - 1.0f) * (y - _parameter.Threshold);
                        }
                        else
                        {
                            tmpGain = (y - state.Unknown10) * ((y - state.Unknown10) * -state.CompressorGainReduction);
                        }

                        z = FloatingPointHelper.DecibelToLinear(tmpGain);
                    }

                    float unknown4New = z;
                    float compressionEmaAlpha;

                    if ((unknown4 - z) <= 0.08f)
                    {
                        compressionEmaAlpha = _parameter.ReleaseCoefficient;

                        if ((unknown4 - z) >= -0.08f)
                        {
                            if (MathF.Abs(compressionGainAverage.Read() - z) >= 0.001f)
                            {
                                unknown4New = unknown4;
                            }

                            compressionEmaAlpha = previousCompressionEmaAlpha;
                        }
                    }
                    else
                    {
                        compressionEmaAlpha = _parameter.AttackCoefficient;
                    }

                    float compressionGain = compressionGainAverage.Update(z, compressionEmaAlpha);

                    for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
                    {
                        *((float*)outputBuffers[channelIndex] + sampleIndex) = channelInput[channelIndex] * compressionGain * state.OutputGain;
                    }

                    unknown4 = unknown4New;
                    previousCompressionEmaAlpha = compressionEmaAlpha;

                    if (!ResultState.IsEmpty)
                    {
                        ref CompressorStatistics statistics = ref MemoryMarshal.Cast<byte, CompressorStatistics>(ResultState.Span[0].SpecificData)[0];

                        statistics.MinimumGain = MathF.Min(statistics.MinimumGain, compressionGain * state.OutputGain);
                        statistics.MaximumMean = MathF.Max(statistics.MaximumMean, mean);
                        
                        Span<float> lastSamplesSpan = statistics.LastSamples.AsSpan();

                        for (int channelIndex = 0; channelIndex < _parameter.ChannelCount; channelIndex++)
                        {
                            lastSamplesSpan[channelIndex] = MathF.Abs(channelInput[channelIndex] * (1f / 32768f));
                        }
                    }
                }

                state.InputMovingAverage = inputMovingAverage;
                state.Unknown4 = unknown4;
                state.CompressionGainAverage = compressionGainAverage;
                state.PreviousCompressionEmaAlpha = previousCompressionEmaAlpha;
            }
            else
            {
                for (int i = 0; i < _parameter.ChannelCount; i++)
                {
                    if (InputBufferIndices[i] != OutputBufferIndices[i])
                    {
                        context.CopyBuffer(OutputBufferIndices[i], InputBufferIndices[i]);
                    }
                }
            }
        }
    }
}
