using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace MoonWorks.Audio
{
	/// <summary>
	/// For streaming long playback.
	/// Can be extended to support custom decoders.
	/// </summary>
	public abstract class StreamingSound : SoundInstance
	{
		private readonly List<IntPtr> queuedBuffers = new List<IntPtr>();
		private const int MINIMUM_BUFFER_CHECK = 3;

		private int PendingBufferCount => queuedBuffers.Count;

		public abstract int BUFFER_SIZE { get; }

		public StreamingSound(
			AudioDevice device,
			ushort formatTag,
			ushort bitsPerSample,
			ushort blockAlign,
			ushort channels,
			uint samplesPerSecond
		) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
		{
			device.AddDynamicSoundInstance(this);
		}

		public override void Play()
		{
			if (State == SoundState.Playing)
			{
				return;
			}

			State = SoundState.Playing;

			Update();
			FAudio.FAudioSourceVoice_Start(Handle, 0, 0);
		}

		public override void Pause()
		{
			if (State == SoundState.Playing)
			{
				FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
				State = SoundState.Paused;
			}
		}

		public override void Stop(bool immediate = true)
		{
			if (immediate)
			{
				FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
				FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
				ClearBuffers();
			}

			State = SoundState.Stopped;
		}

		internal unsafe void Update()
		{
			if (State != SoundState.Playing)
			{
				return;
			}

			FAudio.FAudioSourceVoice_GetState(
				Handle,
				out var state,
				FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
			);

			while (PendingBufferCount > state.BuffersQueued)
			{
				lock (queuedBuffers)
				{
					NativeMemory.Free((void*) queuedBuffers[0]);
					queuedBuffers.RemoveAt(0);
				}
			}

			QueueBuffers();
		}

		protected void QueueBuffers()
		{
			for (
				int i = MINIMUM_BUFFER_CHECK - PendingBufferCount;
				i > 0;
				i -= 1
			)
			{
				AddBuffer();
			}
		}

		protected unsafe void ClearBuffers()
		{
			lock (queuedBuffers)
			{
				foreach (IntPtr buf in queuedBuffers)
				{
					NativeMemory.Free((void*) buf);
				}
				queuedBuffers.Clear();
			}
		}

		protected unsafe void AddBuffer()
		{
			void* buffer = NativeMemory.Alloc((nuint) BUFFER_SIZE);

			AddBuffer(
				buffer,
				BUFFER_SIZE,
				out int filledLengthInBytes,
				out bool reachedEnd
			);

			lock (queuedBuffers)
			{
				queuedBuffers.Add((IntPtr) buffer);
				if (State != SoundState.Stopped)
				{
					FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
					{
						AudioBytes = (uint) filledLengthInBytes,
						pAudioData = (IntPtr) buffer,
						PlayLength = (
							(uint) (filledLengthInBytes /
							Format.nChannels /
							(uint) (Format.wBitsPerSample / 8))
						)
					};

					FAudio.FAudioSourceVoice_SubmitSourceBuffer(
						Handle,
						ref buf,
						IntPtr.Zero
					);
				}
			}


			/* We have reached the end of the file, what do we do? */
			if (reachedEnd)
			{
				OnReachedEnd();
			}
		}

		protected virtual void OnReachedEnd()
		{
			Stop(false);
		}

		protected unsafe abstract void AddBuffer(
			void* buffer,
			int bufferLength, /* in bytes */
			out int filledLength, /* in bytes */
			out bool reachedEnd
		);

		protected override void Destroy()
		{
			Stop(true);
		}
	}
}
