using BMSCore;
using FMOD;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace BMSPlayer
{
    public class SoundControllerFMOD : ISoundController
    {
        private FMOD.System fmod;
        private FMOD.ChannelGroup channelGroup;
        private List<FMOD.Channel> channels;
        private FMOD.Channel bgmChannel;
        private FMOD.Channel previewChannel;
        private FMOD.Channel hitChannel;

        private Dictionary<string, FMOD.Sound> soundLib;

        private static SoundControllerFMOD instance;
        public static SoundControllerFMOD Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new SoundControllerFMOD();
                }
                return instance;
            }
        }

        public SoundControllerFMOD()
        {
            FMODUnity.RuntimeManager.StudioSystem.getCoreSystem(out fmod);

            fmod.setOutput(FMOD.OUTPUTTYPE.AUTODETECT);
            fmod.init(2048, FMOD.INITFLAGS.NORMAL, System.IntPtr.Zero);
            int device;
            Guid guid;
            int systemrate;
            SPEAKERMODE mode;
            int modechn;
            string name;
            fmod.getDriver(out device);
            fmod.getDriverInfo(device, out name, 256, out guid, out systemrate, out mode, out modechn);

            channels = new List<FMOD.Channel>();
            channelGroup = new FMOD.ChannelGroup();
            soundLib = new Dictionary<string, FMOD.Sound>();
        }

        public void UpdateBuffer(uint buffer, int num)
        {
            fmod.close();
            fmod.setDSPBufferSize(buffer, num);
            fmod.init(2048, FMOD.INITFLAGS.NORMAL, (IntPtr)null);
        }

        // Execute after BMSAnalyzer.FullAnalyzer worked
        public void PreloadSound(BMS bms)
        {
            foreach (string val in bms.WavList.Keys)
            {
                string filepath = bms.FolderPath + bms.WavList[val];
                FMOD.Sound snd;
                fmod.createSound(filepath, FMOD.MODE.CREATESAMPLE, out snd);
                bms.WavFilesFM.Add(val, snd);
            }
        }

        public void PreloadSound(string name, string file)
        {
            FMOD.Sound snd;
            fmod.createSound(file, FMOD.MODE.CREATESAMPLE, out snd);
            if (soundLib.ContainsKey(name))
            {
                soundLib.Remove(name);
            }
            soundLib.Add(name, snd);
        }

        public void PreloadSound(string name, AudioClip clip)
        {
            if (soundLib.ContainsKey(name))
            {
                soundLib.Remove(name);
            }
            soundLib.Add(name, ConvertAudioClipToFmod(clip));
        }

        public void PlayKeySound(string wavFile, BMS bms, int line, float volume)
        {
            if (bms.WavFilesFM.ContainsKey(wavFile))
            {
                FMOD.Channel channel;
                fmod.playSound(
                    bms.WavFilesFM[wavFile],
                    channelGroup,
                    false,
                    out channel
                );
                channel.setVolume(Const.VolumeMaster * volume);
                channel.setLoopCount(0);
                channels.Add(channel);
            }
        }

        public void PlaySfxSound(string name)
        {
            FMOD.Channel channel;
            fmod.playSound(
                soundLib[name],
                channelGroup,
                false,
                out channel
            );
            channel.setVolume(Const.VolumeMaster * Const.VolumeSystemSFX);
            channel.setLoopCount(0);
            channels.Add(channel);
        }

        public void PlayHitSound()
        {
            fmod.playSound(
                soundLib["clap"],
                channelGroup,
                false,
                out hitChannel
            );
            hitChannel.setVolume(Const.VolumeMaster * Const.VolumeHitEffect);
            hitChannel.setLoopCount(0);
            channels.Add(hitChannel);
        }

        public void PlayBGMSound(string name)
        {
            fmod.playSound(
                soundLib[name],
                channelGroup,
                false,
                out bgmChannel
            );
            bgmChannel.setVolume(Const.VolumeMaster * Const.VolumeSystemBGM);
            FMOD.RESULT rst = bgmChannel.setMode(FMOD.MODE.LOOP_NORMAL);
        }

        public void PlayPreviewSound(string name)
        {
            fmod.playSound(
                soundLib[name],
                channelGroup,
                false,
                out previewChannel
            );
            previewChannel.setVolume(Const.VolumeMaster * Const.VolumeSystemBGM);
            FMOD.RESULT rst = previewChannel.setMode(FMOD.MODE.LOOP_NORMAL);
        }

        public bool CheckSoundPlaying(BMS bms = null)
        {
            bool isPlaying = false;
            foreach (FMOD.Channel c in channels)
            {
                c.isPlaying(out isPlaying);
                if (isPlaying)
                {
                    break;
                }
            }
            return isPlaying;
        }

        public void StopBGMSound()
        {
            bgmChannel.isPlaying(out bool playing);
            if (playing)
            {
                bgmChannel.stop();
            }
        }

        public void StopPreviewSound()
        {
            previewChannel.isPlaying(out bool playing);
            if (playing)
            {
                previewChannel.stop();
            }
        }

        public void SetVolumeBGMSound(float vol)
        {
            bgmChannel.isPlaying(out bool playing);
            if (playing)
            {
                bgmChannel.setVolume(vol);
            }
        }

        public void StopAll(BMS bms = null)
        {
            foreach (FMOD.Channel c in channels)
            {
                c.stop();
            }
        }

        public void PauseAll(BMS bms = null)
        {
            foreach (FMOD.Channel c in channels)
            {
                c.setPaused(true);
            }
        }

        public void ResumeAll(BMS bms = null)
        {
            foreach (FMOD.Channel c in channels)
            {
                c.setPaused(false);
            }
        }

        public void FreeMemory(BMS bms)
        {
            foreach (FMOD.Sound snd in bms.WavFilesFM.Values)
            {
                snd.release();
            }
        }

        public void FMODErrorCheck(FMOD.RESULT result)
        {
            if (result != FMOD.RESULT.OK)
            {
                Debug.LogError(FMOD.Error.String(result));
            }
        }

        public FMOD.Sound ConvertAudioClipToFmod(AudioClip clip)
        {
            float[] samples = new float[clip.samples * clip.channels];
            clip.GetData(samples, 0);

            uint lenbytes = (uint)(clip.samples * clip.channels * sizeof(float));

            FMOD.CREATESOUNDEXINFO soundinfo = new FMOD.CREATESOUNDEXINFO();
            soundinfo.length = lenbytes;
            soundinfo.format = FMOD.SOUND_FORMAT.PCMFLOAT;
            soundinfo.defaultfrequency = clip.frequency;
            soundinfo.numchannels = clip.channels;
            soundinfo.cbsize = Marshal.SizeOf(typeof(FMOD.CREATESOUNDEXINFO));

            FMOD.RESULT result;
            FMOD.Sound sound;
            result = fmod.createSound(clip.name, FMOD.MODE.OPENUSER, ref soundinfo, out sound);

            result = sound.@lock(0, lenbytes, out IntPtr ptr1, out IntPtr ptr2, out uint len1, out uint len2);
            Marshal.Copy(samples, 0, ptr1, (int)(len1 / sizeof(float)));
            if (len2 > 0)
            {
                Marshal.Copy(samples, (int)(len1 / sizeof(float)), ptr2, (int)(len2 / sizeof(float)));
            }
            result = sound.unlock(ptr1, ptr2, len1, len2);

            result = sound.setMode(FMOD.MODE.LOOP_OFF);

            return sound;
        }

        public void ClearAll()
        {
            foreach (FMOD.Sound item in soundLib.Values)
            {
                item.release();
            }
            foreach (FMOD.Channel ch in channels)
            {
                ch.stop();
                ch.clearHandle();
            }
            channelGroup.clearHandle();
            channelGroup = new FMOD.ChannelGroup();
            soundLib.Clear();
            channels.Clear();
        }
    }
}