﻿using System;
using System.Threading;

using BizHawk.Emulation.Common;
using BizHawk.Emulation.Common.IEmulatorExtensions;
using BizHawk.Client.Common;
using BizHawk.Common;

namespace BizHawk.Client.EmuHawk
{
	public class Sound : IDisposable
	{
		public const int SampleRate = 44100;
		public const int BytesPerSample = 2;
		public const int ChannelCount = 2;
		public const int BlockAlign = BytesPerSample * ChannelCount;

		private bool _disposed;
		private bool _unjamSoundThrottle;

		private readonly ISoundOutput _outputDevice;
		private readonly SoundOutputProvider _outputProvider = new SoundOutputProvider(); // Buffer for Sync sources
		private readonly BufferedAsync _bufferedAsync = new BufferedAsync(); // Buffer for Async sources
		private IBufferedSoundProvider _bufferedProvider; // One of the preceding buffers, or null if no source is set

		public Sound(IntPtr mainWindowHandle)
		{
			if (OSTailoredCode.CurrentOS == OSTailoredCode.DistinctOS.Windows)
			{
				if (Global.Config.SoundOutputMethod == Config.ESoundOutputMethod.OpenAL)
					_outputDevice = new OpenALSoundOutput(this);
				if (Global.Config.SoundOutputMethod == Config.ESoundOutputMethod.DirectSound)
					_outputDevice = new DirectSoundSoundOutput(this, mainWindowHandle);
				if (Global.Config.SoundOutputMethod == Config.ESoundOutputMethod.XAudio2)
					_outputDevice = new XAudio2SoundOutput(this);
			}
			else _outputDevice = new OpenALSoundOutput(this); // at the moment unix/mono can only support OpenAL (so ignore whatever is set in the config)

			if (_outputDevice == null)
				_outputDevice = new DummySoundOutput(this);
		}

		public void Dispose()
		{
			if (_disposed) return;

			StopSound();

			_outputDevice.Dispose();

			_disposed = true;
		}

		public bool IsStarted { get; private set; }

		public void StartSound()
		{
			if (_disposed) return;
			if (!Global.Config.SoundEnabled) return;
			if (IsStarted) return;

			_outputDevice.StartSound();

			_outputProvider.MaxSamplesDeficit = _outputDevice.MaxSamplesDeficit;

			Global.SoundMaxBufferDeficitMs = (int)Math.Ceiling(SamplesToMilliseconds(_outputDevice.MaxSamplesDeficit));

			IsStarted = true;
		}

		public void StopSound()
		{
			if (!IsStarted) return;

			_outputDevice.StopSound();

			if (_bufferedProvider != null) _bufferedProvider.DiscardSamples();

			Global.SoundMaxBufferDeficitMs = 0;

			IsStarted = false;
		}

		/// <summary>
		/// Attaches a new input pin which will run either in sync or async mode depending
		/// on its SyncMode property. Once attached, the sync mode must not change unless
		/// the pin is re-attached.
		/// </summary>
		public void SetInputPin(ISoundProvider source)
		{
			if (_bufferedProvider != null)
			{
				_bufferedProvider.BaseSoundProvider = null;
				_bufferedProvider.DiscardSamples();
				_bufferedProvider = null;
			}

			if (source == null) return;

			if (source.SyncMode == SyncSoundMode.Sync)
			{
				_bufferedProvider = _outputProvider;
			}
			else if (source.SyncMode == SyncSoundMode.Async)
			{
				_bufferedAsync.RecalculateMagic(Global.Emulator.VsyncRate());
				_bufferedProvider = _bufferedAsync;
			}
			else throw new InvalidOperationException("Unsupported sync mode.");

			_bufferedProvider.BaseSoundProvider = source;
		}

		public bool LogUnderruns { get; set; }

		internal void HandleInitializationOrUnderrun(bool isUnderrun, ref int samplesNeeded)
		{
			// Fill device buffer with silence but leave enough room for one frame
			int samplesPerFrame = (int)Math.Round(SampleRate / (double)Global.Emulator.VsyncRate());
			int silenceSamples = Math.Max(samplesNeeded - samplesPerFrame, 0);
			_outputDevice.WriteSamples(new short[silenceSamples * 2], silenceSamples);
			samplesNeeded -= silenceSamples;
			_unjamSoundThrottle = isUnderrun;

			if (isUnderrun)
			{
				if (LogUnderruns) Console.WriteLine("Sound underrun detected!");
				_outputProvider.OnVolatility();
			}
		}

		public void UpdateSound(float atten)
		{
			if (!Global.Config.SoundEnabled || !IsStarted || _bufferedProvider == null || _disposed)
			{
				if (_bufferedProvider != null) _bufferedProvider.DiscardSamples();
				return;
			}

			if (atten < 0) atten = 0;
			if (atten > 1) atten = 1;
			_outputDevice.ApplyVolumeSettings(atten);

			short[] samples;
			int samplesNeeded = _outputDevice.CalculateSamplesNeeded();
			int samplesProvided;

			if (atten == 0)
			{
				samples = new short[samplesNeeded * ChannelCount];
				samplesProvided = samplesNeeded;

				_bufferedProvider.DiscardSamples();
			}
			else if (_bufferedProvider == _outputProvider)
			{
				if (Global.Config.SoundThrottle)
				{
					_outputProvider.BaseSoundProvider.GetSamplesSync(out samples, out samplesProvided);

					if (Global.DisableSecondaryThrottling && samplesProvided > samplesNeeded)
					{
						return;
					}

					while (samplesProvided > samplesNeeded)
					{
						Thread.Sleep((samplesProvided - samplesNeeded) / (SampleRate / 1000)); // Let the audio clock control sleep time
						samplesNeeded = _outputDevice.CalculateSamplesNeeded();
						if (_unjamSoundThrottle)
						{
							//may be garbage, but what can we do?
							samplesProvided = samplesNeeded;
							break;
						}
					}
					_unjamSoundThrottle = false;
				}
				else
				{
					if (Global.DisableSecondaryThrottling) // This indicates rewind or fast-forward
					{
						_outputProvider.OnVolatility();
					}
					_outputProvider.GetSamples(samplesNeeded, out samples, out samplesProvided);
				}
			}
			else if (_bufferedProvider == _bufferedAsync)
			{
				samples = new short[samplesNeeded * ChannelCount];

				_bufferedAsync.GetSamplesAsync(samples);

				samplesProvided = samplesNeeded;
			}
			else
			{
				return;
			}

			_outputDevice.WriteSamples(samples, samplesProvided);
		}

		public static int MillisecondsToSamples(int milliseconds)
		{
			return milliseconds * SampleRate / 1000;
		}

		public static double SamplesToMilliseconds(int samples)
		{
			return samples * 1000.0 / SampleRate;
		}
	}
}
