using Ryujinx.Common;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System;

namespace Ryujinx.Horizon.Sdk.Codec.Detail
{
    partial class HardwareOpusDecoderManager : IHardwareOpusDecoderManager
    {
        [CmifCommand(0)]
        public Result OpenHardwareOpusDecoder(
            out IHardwareOpusDecoder decoder,
            HardwareOpusDecoderParameterInternal parameter,
            [CopyHandle] int workBufferHandle,
            int workBufferSize)
        {
            decoder = null;

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidSampleRate;
            }

            if (!IsValidChannelCount(parameter.ChannelsCount))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidChannelCount;
            }

            decoder = new HardwareOpusDecoder(parameter.SampleRate, parameter.ChannelsCount, workBufferHandle);

            return Result.Success;
        }

        [CmifCommand(1)]
        public Result GetWorkBufferSize(out int size, HardwareOpusDecoderParameterInternal parameter)
        {
            size = 0;

            if (!IsValidChannelCount(parameter.ChannelsCount))
            {
                return CodecResult.InvalidChannelCount;
            }

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                return CodecResult.InvalidSampleRate;
            }

            int opusDecoderSize = GetOpusDecoderSize(parameter.ChannelsCount);

            int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
            int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * 1920 / sampleRateRatio : 0, 64);
            size = opusDecoderSize + 1536 + frameSize;

            return Result.Success;
        }

        [CmifCommand(2)] // 3.0.0+
        public Result OpenHardwareOpusDecoderForMultiStream(
            out IHardwareOpusDecoder decoder,
            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x110)] in HardwareOpusMultiStreamDecoderParameterInternal parameter,
            [CopyHandle] int workBufferHandle,
            int workBufferSize)
        {
            decoder = null;

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidSampleRate;
            }

            if (!IsValidMultiChannelCount(parameter.ChannelsCount))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidChannelCount;
            }

            if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidNumberOfStreams;
            }

            decoder = new HardwareOpusDecoder(
                parameter.SampleRate,
                parameter.ChannelsCount,
                parameter.NumberOfStreams,
                parameter.NumberOfStereoStreams,
                parameter.ChannelMappings.AsSpan().ToArray(),
                workBufferHandle);

            return Result.Success;
        }

        [CmifCommand(3)] // 3.0.0+
        public Result GetWorkBufferSizeForMultiStream(
            out int size,
            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x110)] in HardwareOpusMultiStreamDecoderParameterInternal parameter)
        {
            size = 0;

            if (!IsValidMultiChannelCount(parameter.ChannelsCount))
            {
                return CodecResult.InvalidChannelCount;
            }

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                return CodecResult.InvalidSampleRate;
            }

            if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
            {
                return CodecResult.InvalidSampleRate;
            }

            int opusDecoderSize = GetOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams);

            int streamSize = BitUtils.AlignUp(parameter.NumberOfStreams * 1500, 64);
            int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
            int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * 1920 / sampleRateRatio : 0, 64);
            size = opusDecoderSize + streamSize + frameSize;

            return Result.Success;
        }

        [CmifCommand(4)] // 12.0.0+
        public Result OpenHardwareOpusDecoderEx(
            out IHardwareOpusDecoder decoder,
            HardwareOpusDecoderParameterInternalEx parameter,
            [CopyHandle] int workBufferHandle,
            int workBufferSize)
        {
            decoder = null;

            if (!IsValidChannelCount(parameter.ChannelsCount))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidChannelCount;
            }

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidSampleRate;
            }

            decoder = new HardwareOpusDecoder(parameter.SampleRate, parameter.ChannelsCount, workBufferHandle);

            return Result.Success;
        }

        [CmifCommand(5)] // 12.0.0+
        public Result GetWorkBufferSizeEx(out int size, HardwareOpusDecoderParameterInternalEx parameter)
        {
            return GetWorkBufferSizeExImpl(out size, in parameter, fromDsp: false);
        }

        [CmifCommand(6)] // 12.0.0+
        public Result OpenHardwareOpusDecoderForMultiStreamEx(
            out IHardwareOpusDecoder decoder,
            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter,
            [CopyHandle] int workBufferHandle,
            int workBufferSize)
        {
            decoder = null;

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidSampleRate;
            }

            if (!IsValidMultiChannelCount(parameter.ChannelsCount))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidChannelCount;
            }

            if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
            {
                HorizonStatic.Syscall.CloseHandle(workBufferHandle);

                return CodecResult.InvalidNumberOfStreams;
            }

            decoder = new HardwareOpusDecoder(
                parameter.SampleRate,
                parameter.ChannelsCount,
                parameter.NumberOfStreams,
                parameter.NumberOfStereoStreams,
                parameter.ChannelMappings.AsSpan().ToArray(),
                workBufferHandle);

            return Result.Success;
        }

        [CmifCommand(7)] // 12.0.0+
        public Result GetWorkBufferSizeForMultiStreamEx(
            out int size,
            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter)
        {
            return GetWorkBufferSizeForMultiStreamExImpl(out size, in parameter, fromDsp: false);
        }

        [CmifCommand(8)] // 16.0.0+
        public Result GetWorkBufferSizeExEx(out int size, HardwareOpusDecoderParameterInternalEx parameter)
        {
            return GetWorkBufferSizeExImpl(out size, in parameter, fromDsp: true);
        }

        [CmifCommand(9)] // 16.0.0+
        public Result GetWorkBufferSizeForMultiStreamExEx(
            out int size,
            [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer, 0x118)] in HardwareOpusMultiStreamDecoderParameterInternalEx parameter)
        {
            return GetWorkBufferSizeForMultiStreamExImpl(out size, in parameter, fromDsp: true);
        }

        private Result GetWorkBufferSizeExImpl(out int size, in HardwareOpusDecoderParameterInternalEx parameter, bool fromDsp)
        {
            size = 0;

            if (!IsValidChannelCount(parameter.ChannelsCount))
            {
                return CodecResult.InvalidChannelCount;
            }

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                return CodecResult.InvalidSampleRate;
            }

            int opusDecoderSize = fromDsp ? GetDspOpusDecoderSize(parameter.ChannelsCount) : GetOpusDecoderSize(parameter.ChannelsCount);

            int frameSizeMono48KHz = parameter.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920;
            int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
            int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * frameSizeMono48KHz / sampleRateRatio : 0, 64);
            size = opusDecoderSize + 1536 + frameSize;

            return Result.Success;
        }

        private Result GetWorkBufferSizeForMultiStreamExImpl(out int size, in HardwareOpusMultiStreamDecoderParameterInternalEx parameter, bool fromDsp)
        {
            size = 0;

            if (!IsValidMultiChannelCount(parameter.ChannelsCount))
            {
                return CodecResult.InvalidChannelCount;
            }

            if (!IsValidSampleRate(parameter.SampleRate))
            {
                return CodecResult.InvalidSampleRate;
            }

            if (!IsValidNumberOfStreams(parameter.NumberOfStreams, parameter.NumberOfStereoStreams, parameter.ChannelsCount))
            {
                return CodecResult.InvalidSampleRate;
            }

            int opusDecoderSize = fromDsp
                ? GetDspOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams)
                : GetOpusMultistreamDecoderSize(parameter.NumberOfStreams, parameter.NumberOfStereoStreams);

            int frameSizeMono48KHz = parameter.Flags.HasFlag(OpusDecoderFlags.LargeFrameSize) ? 5760 : 1920;
            int streamSize = BitUtils.AlignUp(parameter.NumberOfStreams * 1500, 64);
            int sampleRateRatio = parameter.SampleRate != 0 ? 48000 / parameter.SampleRate : 0;
            int frameSize = BitUtils.AlignUp(sampleRateRatio != 0 ? parameter.ChannelsCount * frameSizeMono48KHz / sampleRateRatio : 0, 64);
            size = opusDecoderSize + streamSize + frameSize;

            return Result.Success;
        }

        private static int GetDspOpusDecoderSize(int channelsCount)
        {
            // TODO: Figure out the size returned here.
            // Not really important because we don't use the work buffer, and the size being lower is fine.

            return 0;
        }

        private static int GetDspOpusMultistreamDecoderSize(int streams, int coupledStreams)
        {
            // TODO: Figure out the size returned here.
            // Not really important because we don't use the work buffer, and the size being lower is fine.

            return 0;
        }

        private static int GetOpusDecoderSize(int channelsCount)
        {
            const int SilkDecoderSize = 0x2160;

            if (channelsCount < 1 || channelsCount > 2)
            {
                return 0;
            }

            int celtDecoderSize = GetCeltDecoderSize(channelsCount);
            int opusDecoderSize = GetOpusDecoderAllocSize(channelsCount) | 0x50;

            return opusDecoderSize + SilkDecoderSize + celtDecoderSize;
        }

        private static int GetOpusMultistreamDecoderSize(int streams, int coupledStreams)
        {
            if (streams < 1 || coupledStreams > streams || coupledStreams < 0)
            {
                return 0;
            }

            int coupledSize = GetOpusDecoderSize(2);
            int monoSize = GetOpusDecoderSize(1);

            return Align4(monoSize - GetOpusDecoderAllocSize(1)) * (streams - coupledStreams) +
                Align4(coupledSize - GetOpusDecoderAllocSize(2)) * coupledStreams + 0xb920;
        }

        private static int Align4(int value)
        {
            return BitUtils.AlignUp(value, 4);
        }

        private static int GetOpusDecoderAllocSize(int channelsCount)
        {
            return channelsCount * 0x800 + 0x4800;
        }

        private static int GetCeltDecoderSize(int channelsCount)
        {
            const int DecodeBufferSize = 0x2030;
            const int Overlap = 120;
            const int EBandsCount = 21;

            return (DecodeBufferSize + Overlap * 4) * channelsCount + EBandsCount * 16 + 0x54;
        }

        private static bool IsValidChannelCount(int channelsCount)
        {
            return channelsCount > 0 && channelsCount <= 2;
        }

        private static bool IsValidMultiChannelCount(int channelsCount)
        {
            return channelsCount > 0 && channelsCount <= 255;
        }

        private static bool IsValidSampleRate(int sampleRate)
        {
            switch (sampleRate)
            {
                case 8000:
                case 12000:
                case 16000:
                case 24000:
                case 48000:
                    return true;
            }

            return false;
        }

        private static bool IsValidNumberOfStreams(int numberOfStreams, int numberOfStereoStreams, int channelsCount)
        {
            return numberOfStreams > 0 &&
                numberOfStreams + numberOfStereoStreams <= channelsCount &&
                numberOfStereoStreams >= 0 &&
                numberOfStereoStreams <= numberOfStreams;
        }
    }
}