using OpenTK.Audio;
using OpenTK.Audio.OpenAL;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;

namespace Ryujinx.Audio
{
    /// <summary>
    /// An audio renderer that uses OpenAL as the audio backend
    /// </summary>
    public class OpenALAudioOut : IAalOutput, IDisposable
    {
        private const int MaxTracks = 256;

        private const int MaxReleased = 32;

        private AudioContext Context;

        private class Track : IDisposable
        {
            public int SourceId { get; private set; }

            public int SampleRate { get; private set; }

            public ALFormat Format { get; private set; }

            private ReleaseCallback Callback;

            public PlaybackState State { get; set; }

            private ConcurrentDictionary<long, int> Buffers;

            private Queue<long> QueuedTagsQueue;

            private Queue<long> ReleasedTagsQueue;

            private bool Disposed;

            public Track(int SampleRate, ALFormat Format, ReleaseCallback Callback)
            {
                this.SampleRate = SampleRate;
                this.Format     = Format;
                this.Callback   = Callback;

                State = PlaybackState.Stopped;

                SourceId = AL.GenSource();

                Buffers = new ConcurrentDictionary<long, int>();

                QueuedTagsQueue = new Queue<long>();

                ReleasedTagsQueue = new Queue<long>();
            }

            public bool ContainsBuffer(long Tag)
            {
                foreach (long QueuedTag in QueuedTagsQueue)
                {
                    if (QueuedTag == Tag)
                    {
                        return true;
                    }
                }

                return false;
            }

            public long[] GetReleasedBuffers(int Count)
            {
                AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int ReleasedCount);

                ReleasedCount += ReleasedTagsQueue.Count;

                if (Count > ReleasedCount)
                {
                    Count = ReleasedCount;
                }

                List<long> Tags = new List<long>();

                while (Count-- > 0 && ReleasedTagsQueue.TryDequeue(out long Tag))
                {
                    Tags.Add(Tag);
                }

                while (Count-- > 0 && QueuedTagsQueue.TryDequeue(out long Tag))
                {
                    AL.SourceUnqueueBuffers(SourceId, 1);

                    Tags.Add(Tag);
                }

                return Tags.ToArray();
            }

            public int AppendBuffer(long Tag)
            {
                if (Disposed)
                {
                    throw new ObjectDisposedException(nameof(Track));
                }

                int Id = AL.GenBuffer();

                Buffers.AddOrUpdate(Tag, Id, (Key, OldId) =>
                {
                    AL.DeleteBuffer(OldId);

                    return Id;
                });

                QueuedTagsQueue.Enqueue(Tag);

                return Id;
            }

            public void CallReleaseCallbackIfNeeded()
            {
                AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out int ReleasedCount);

                if (ReleasedCount > 0)
                {
                    // If we signal, then we also need to have released buffers available
                    // to return when GetReleasedBuffers is called.
                    // If playback needs to be re-started due to all buffers being processed,
                    // then OpenAL zeros the counts (ReleasedCount), so we keep it on the queue.
                    while (ReleasedCount-- > 0 && QueuedTagsQueue.TryDequeue(out long Tag))
                    {
                        AL.SourceUnqueueBuffers(SourceId, 1);

                        ReleasedTagsQueue.Enqueue(Tag);
                    }

                    Callback();
                }
            }

            public void Dispose()
            {
                Dispose(true);
            }

            protected virtual void Dispose(bool Disposing)
            {
                if (Disposing && !Disposed)
                {
                    Disposed = true;

                    AL.DeleteSource(SourceId);

                    foreach (int Id in Buffers.Values)
                    {
                        AL.DeleteBuffer(Id);
                    }
                }
            }
        }

        private ConcurrentDictionary<int, Track> Tracks;

        private Thread AudioPollerThread;

        private bool KeepPolling;

        public OpenALAudioOut()
        {
            Context = new AudioContext();

            Tracks = new ConcurrentDictionary<int, Track>();

            KeepPolling = true;

            AudioPollerThread = new Thread(AudioPollerWork);

            AudioPollerThread.Start();
        }

        /// <summary>
        /// True if OpenAL is supported on the device.
        /// </summary>
        public static bool IsSupported
        {
            get
            {
                try
                {
                    return AudioContext.AvailableDevices.Count > 0;
                }
                catch
                {
                    return false;
                }
            }
        }

        private void AudioPollerWork()
        {
            do
            {
                foreach (Track Td in Tracks.Values)
                {
                    lock (Td)
                    {
                        Td.CallReleaseCallbackIfNeeded();
                    }
                }

                // If it's not slept it will waste cycles.
                Thread.Sleep(10);
            }
            while (KeepPolling);

            foreach (Track Td in Tracks.Values)
            {
                Td.Dispose();
            }

            Tracks.Clear();
        }

        public int OpenTrack(int SampleRate, int Channels, ReleaseCallback Callback)
        {
            Track Td = new Track(SampleRate, GetALFormat(Channels), Callback);

            for (int Id = 0; Id < MaxTracks; Id++)
            {
                if (Tracks.TryAdd(Id, Td))
                {
                    return Id;
                }
            }

            return -1;
        }

        private ALFormat GetALFormat(int Channels)
        {
            switch (Channels)
            {
                case 1: return ALFormat.Mono16;
                case 2: return ALFormat.Stereo16;
                case 6: return ALFormat.Multi51Chn16Ext;
            }

            throw new ArgumentOutOfRangeException(nameof(Channels));
        }

        public void CloseTrack(int Track)
        {
            if (Tracks.TryRemove(Track, out Track Td))
            {
                lock (Td)
                {
                    Td.Dispose();
                }
            }
        }

        public bool ContainsBuffer(int Track, long Tag)
        {
            if (Tracks.TryGetValue(Track, out Track Td))
            {
                lock (Td)
                {
                    return Td.ContainsBuffer(Tag);
                }
            }

            return false;
        }

        public long[] GetReleasedBuffers(int Track, int MaxCount)
        {
            if (Tracks.TryGetValue(Track, out Track Td))
            {
                lock (Td)
                {
                    return Td.GetReleasedBuffers(MaxCount);
                }
            }

            return null;
        }

        public void AppendBuffer<T>(int Track, long Tag, T[] Buffer) where T : struct
        {
            if (Tracks.TryGetValue(Track, out Track Td))
            {
                lock (Td)
                {
                    int BufferId = Td.AppendBuffer(Tag);

                    int Size = Buffer.Length * Marshal.SizeOf<T>();

                    AL.BufferData<T>(BufferId, Td.Format, Buffer, Size, Td.SampleRate);

                    AL.SourceQueueBuffer(Td.SourceId, BufferId);

                    StartPlaybackIfNeeded(Td);
                }
            }
        }

        public void Start(int Track)
        {
            if (Tracks.TryGetValue(Track, out Track Td))
            {
                lock (Td)
                {
                    Td.State = PlaybackState.Playing;

                    StartPlaybackIfNeeded(Td);
                }
            }
        }

        private void StartPlaybackIfNeeded(Track Td)
        {
            AL.GetSource(Td.SourceId, ALGetSourcei.SourceState, out int StateInt);

            ALSourceState State = (ALSourceState)StateInt;

            if (State != ALSourceState.Playing && Td.State == PlaybackState.Playing)
            {
                AL.SourcePlay(Td.SourceId);
            }
        }

        public void Stop(int Track)
        {
            if (Tracks.TryGetValue(Track, out Track Td))
            {
                lock (Td)
                {
                    Td.State = PlaybackState.Stopped;

                    AL.SourceStop(Td.SourceId);
                }
            }
        }

        public PlaybackState GetState(int Track)
        {
            if (Tracks.TryGetValue(Track, out Track Td))
            {
                return Td.State;
            }

            return PlaybackState.Stopped;
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool Disposing)
        {
            if (Disposing)
            {
                KeepPolling = false;
            }
        }
    }
}