#pragma once
#include <bfc/platform/types.h>
#include "../Winamp/in2.h"
#include "../Winamp/out.h"
#include "SpillBuffer.h"
#include <assert.h>

/* A class to manage Winamp input plugin audio output
** It handles the following for you:
** * Ensuring that Vis data is sent in chunks of 576
** * Dealing with gapless audio 
** (you need to pass in the number of pre-delay and post-delay samples)
** * dealing with the DSP plugin
** * Waiting for CanWrite()
** * dealing with inter-timestamps 
** e.g. you pass it >576 samples and it can give you a timestamp based on the divided chunk position

to use, you need to derive from a class that declares
int WaitOrAbort(int time_in_ms);
return 0 on success, non-zero when you need to abort.  the return value is passed back through Write()
*/

namespace nu // namespace it since "AudioOutput" isn't a unique enough name
{
	template <class wait_t>
	class AudioOutput : public wait_t
	{
	public:
		AudioOutput( In_Module *plugin ) : plugin( plugin )
		{
			Init( nullptr );
		}

		~AudioOutput()
		{
			post_buffer.reset();
			buffer576.reset();
		}

		/* Initializes and sets the output plugin pointer
		** for most input plugins, the nu::AudioOutput object will be a global, 
		** so this will be necessary to call at the start of Play thread */
		void Init( Out_Module *_output )
		{
			output          = _output;
			audio_opened    = false;
			first_timestamp = 0;
			sample_size     = 0;
			output_latency  = 0;

			post_buffer.reset();
			buffer576.reset();

			cut_size        = 0;
			pre_cut_size    = 0;
			pre_cut         = 0;
			decoder_delay   = 0;
			channels        = 0;
			sample_rate     = 0;
			bps             = 0;
		}

		/* sets end-of-stream delay (in samples)
		** WITHOUT componesating for post-delay. 
		** some filetypes (e.g. iTunes MP4) store gapless info this way */
		void SetPostDelay(int postSize)
		{
			if (postSize < 0)
			{
				postSize = 0;
			}
			else if (postSize)
			{
				if (sample_size)
					post_buffer.reserve(postSize*sample_size);

				cut_size = postSize;
			}
		}

		/* set end-of-stream zero padding, in samples
		** compensates for decoder delay */
		void SetZeroPadding(int postSize)
		{
			postSize -= decoder_delay;
			if (postSize < 0)
			{
				postSize = 0;
			}
			SetPostDelay(postSize);
		}

		/* set decoder delay, initial zero samples and end-of-stream zero samples, all in one shot
		** adjusts zero samples for decoder delay. call SetDelays() if your zero samples are already compensated */
		void SetGapless(int decoderDelaySize, int preSize, int postSize)
		{
			decoder_delay = decoderDelaySize;
			SetZeroPadding(postSize);

			pre_cut_size = preSize;
			pre_cut = pre_cut_size + decoder_delay;
		}

		/* set decoder delay, initial delay and end-of-stream delay, all in one shot
		** WITHOUT componesating for post-delay. 
		** some filetypes (e.g. iTunes MP4) store gapless info this way */
		void SetDelays(int decoderDelaySize, int preSize, int postSize)
		{
			decoder_delay = decoderDelaySize;
			SetPostDelay(postSize);

			pre_cut_size = preSize;
			pre_cut = pre_cut_size;
		}

		/* Call on seek */
		void Flush(int time_in_ms)
		{
			if (audio_opened)
			{
				pre_cut = pre_cut_size;

				output->Flush(time_in_ms);
				first_timestamp = 0; // once we've flushed, we should be accurate so no need for this anymore
				buffer576.clear();
				post_buffer.clear();
			}
			else
				first_timestamp = time_in_ms;
		}

		bool Opened() const
		{
			return audio_opened;
		}

		int GetLatency() const
		{
			return output_latency;
		}

		int GetFirstTimestamp() const
		{
			return first_timestamp;
		}

		/* timestamp is meant to be the first timestamp according to the containing file format
		** e.g. many MP4 videos start on 12ms or something, for accurate a/v syncing */
		bool Open(int timestamp, int channels, int sample_rate, int bps, int buffer_len_ms=-1, int pre_buffer_ms=-1)
		{
			if (!audio_opened)
			{
				int latency = output->Open(sample_rate, channels, bps, buffer_len_ms, pre_buffer_ms);
				if (latency < 0)
					return false;
				plugin->SAVSAInit(latency, sample_rate);
				plugin->VSASetInfo(sample_rate, channels);
				output->SetVolume(-666);
				plugin->SetInfo(-1, sample_rate / 1000, channels, /* TODO? 0*/1);

				output_latency = latency;
				first_timestamp = timestamp;
				sample_size = channels*bps / 8;
				this->channels=channels;
				this->sample_rate=sample_rate;
				this->bps=bps;
				SetPostDelay((int)cut_size); // set this again now that we know sample_size, so buffers get allocated correctly
				buffer576.reserve(576*sample_size);
				audio_opened=true;
			}
			return audio_opened;
		}

		void Close()
		{
			if (audio_opened && output)
			{
				output->Close();
				plugin->SAVSADeInit();
			}
			output = 0;
			first_timestamp = 0;
		}

		/* outSize is in bytes
		** */
		int Write(char *out, size_t outSize)
		{
			if (!out && !outSize)
			{
				/* --- write contents of buffered audio (end-zero-padding buffer) */
				if (!post_buffer.empty())
				{
					void *buffer = 0;
					size_t len = 0;
					if (post_buffer.get(&buffer, &len))
					{
						int ret = Write576((char *)buffer, len);
						if (ret != 0)
							return ret;
					}
				}

				/* --- write any remaining data in 576 spill buffer (skip vis) */
				if (!buffer576.empty())
				{
					void *buffer = 0;
					size_t len = 0;
					if (buffer576.get(&buffer, &len))
					{
						int ret = WriteOutput((char *)buffer, len);
						if (ret != 0)
							return ret;
					}
				}

				output->Write(0, 0);
				return 0;
			}

			// this probably should not happen but have seen it in some crash reports
			if (!sample_size)
				return 0;

			assert((outSize % sample_size) == 0);
			size_t outSamples = outSize / sample_size;

			/* --- cut pre samples, if necessary --- */
			size_t pre  = min(pre_cut, outSamples);
			out        += pre * sample_size;
			outSize    -= pre  * sample_size;
			pre_cut    -= pre;
			//outSize = outSamples * sample_size;

			// do we will have samples to output after cutting pre-delay?
			if (!outSize)
				return 0;

			/* --- if we don't have enough to fully fill the end-zero-padding buffer, go ahead and fill --- */
			if (outSize < post_buffer.length())
			{
				size_t bytes_written = post_buffer.write(out, outSize);
				out+=bytes_written;
				outSize-=bytes_written;
			}

			// if we're out of samples, go ahead and bail
			if (!outSize)
				return 0;

			/* --- write contents of buffered audio (end-zero-padding buffer) */
			if (!post_buffer.empty())
			{
				void *buffer = 0;
				size_t len = 0;
				if (post_buffer.get(&buffer, &len))
				{
					int ret = Write576((char *)buffer, len);
					if (ret != 0)
						return ret;
				}
			}

			/* --- make sure we have enough samples left over to fill our post-zero-padding buffer --- */
			size_t remainingFill = /*cut_size - */post_buffer.remaining();
			int outWrite = max(0, (int)outSize - (int)remainingFill);

			/* --- write the output that doesn't end up in the post buffer */
			if (outWrite)
			{
				int ret = Write576(out, outWrite);
				if (ret != 0)
					return ret;
			}
			out += outWrite;
			outSize -= outWrite;

			/* --- write whatever is left over into the end-zero-padding buffer --- */
			if (outSize)
			{
				post_buffer.write(out, outSize);
			}
			return 0;
		}

		/* meant to be called after Write(0,0) */
		int WaitWhilePlaying()
		{
			while (output->IsPlaying())
			{
				int ret = WaitOrAbort(10);
				if (ret != 0)
					return ret;

				output->CanWrite();		// some output drivers need CanWrite
				// to be called on a regular basis.
			}
			return 0;
		}
	private:
		/* helper methods */
		int WaitForOutput(int write_size_bytes)
		{
			while (output->CanWrite() < write_size_bytes)
			{
				int ret = WaitOrAbort(55);
				if (ret != 0)
					return ret;
			}
			return 0;
		}


		/* writes one chunk (576 samples) to the output plugin, waiting as necessary */
		int WriteOutput(char *buffer, size_t len)
		{
			int ret = WaitForOutput((int)len);
			if (ret != 0)
				return ret;

			// write vis data before so we guarantee 576 samples
			if (len == 576*sample_size)
			{
				plugin->SAAddPCMData(buffer, channels, bps, output->GetWrittenTime() + first_timestamp);
				plugin->VSAAddPCMData(buffer, channels, bps, output->GetWrittenTime() + first_timestamp);
			}

			if (plugin->dsp_isactive())
				len = sample_size * plugin->dsp_dosamples((short *)buffer, (int)(len / sample_size), bps, channels, sample_rate);

			output->Write(buffer, (int)len);
			return 0;
		}

		/* given a large buffer, writes 576 sample chunks to the vis, dsp and output plugin */
		int Write576(char *buffer, size_t out_size)
		{
			/* if we have some stuff leftover in the 576 sample spill buffer, fill it up */
			if (!buffer576.empty())
			{
				size_t bytes_written = buffer576.write(buffer, out_size);
				out_size -= bytes_written;
				buffer += bytes_written;
			}

			if (buffer576.full())
			{
				void *buffer = 0;
				size_t len = 0;
				if (buffer576.get(&buffer, &len))
				{
					int ret = WriteOutput((char *)buffer, len);
					if (ret != 0)
						return ret;
				}
			}

			while (out_size >= 576*sample_size)
			{
				int ret = WriteOutput(buffer, 576*sample_size);
				if (ret != 0)
					return ret;

				out_size -= 576*sample_size;
				buffer+=576*sample_size;
			}

			if (out_size) 
			{
				assert(out_size < 576*sample_size);
				buffer576.write(buffer, out_size);
			}
			return 0;
		}

	private:
		Out_Module *output;
		In_Module *plugin;
		SpillBuffer post_buffer, buffer576;
		size_t cut_size;
		size_t pre_cut, pre_cut_size, decoder_delay;
		bool audio_opened;
		int first_timestamp; /* timestamp of the first decoded audio frame, necessary for accurate video syncing */
		size_t sample_size; /* size, in bytes, of one sample of audio (channels*bps/8) */
		int output_latency; /* as returned from Out_Module::Open() */
		int channels, sample_rate, bps;
	};
}