using OpenTK.Audio.OpenAL;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Ryujinx.Audio
{
    internal class OpenALAudioTrack : IDisposable
    {
        public int           SourceId   { get; private set; }
        public int           SampleRate { get; private set; }
        public ALFormat      Format     { get; private set; }
        public PlaybackState State      { get; set; }

        public int HardwareChannels { get; }
        public int VirtualChannels { get; }
        public uint BufferCount => (uint)_buffers.Count;
        public ulong PlayedSampleCount { get; set; }

        private ReleaseCallback _callback;

        private ConcurrentDictionary<long, int> _buffers;

        private Queue<long> _queuedTagsQueue;
        private Queue<long> _releasedTagsQueue;

        private bool _disposed;

        public OpenALAudioTrack(int sampleRate, ALFormat format, int hardwareChannels, int virtualChannels, ReleaseCallback callback)
        {
            SampleRate = sampleRate;
            Format     = format;
            State      = PlaybackState.Stopped;
            SourceId   = AL.GenSource();

            HardwareChannels = hardwareChannels;
            VirtualChannels = virtualChannels;

            _callback = callback;

            _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(GetType().Name);
            }

            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 bool FlushBuffers()
        {
            while (_queuedTagsQueue.TryDequeue(out long tag))
            {
                _releasedTagsQueue.Enqueue(tag);
            }

            _callback();

            foreach (var buffer in _buffers)
            {
                AL.DeleteBuffer(buffer.Value);
            }

            bool heldBuffers = _buffers.Count > 0;

            _buffers.Clear();

            return heldBuffers;
        }

        public void SetVolume(float volume)
        {
            AL.Source(SourceId, ALSourcef.Gain, volume);
        }

        public float GetVolume()
        {
            AL.GetSource(SourceId, ALSourcef.Gain, out float volume);

            return volume;
        }

        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);
                }
            }
        }
    }
}