using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Numerics;

namespace BizHawk.Client.EmuHawk
{
	/// <summary>
	/// implements a simple muxer for the NUT media format
	/// http://ffmpeg.org/~michael/nut.txt
	/// </summary>
	class NutMuxer
	{
		// this code isn't really any good for general purpose nut creation

		#region simple buffer reuser

		public class ReusableBufferPool<T>
		{
			private List<T[]> _available = new List<T[]>();
			private ICollection<T[]> _inuse = new HashSet<T[]>();

			private readonly int _capacity;

			/// <summary>
			/// 
			/// </summary>
			/// <param name="capacity">total number of buffers to keep around</param>
			public ReusableBufferPool(int capacity)
			{
				_capacity = capacity;
			}

			private T[] GetBufferInternal(int length, bool zerofill, Func<T[], bool> criteria)
			{
				if (_inuse.Count == _capacity)
					throw new InvalidOperationException();

				T[] candidate = _available.FirstOrDefault(criteria);
				if (candidate == null)
				{
					if (_available.Count + _inuse.Count == _capacity)
					{
						// out of space! should not happen often
						Console.WriteLine("Purging");
						_available.Clear();
					}
					candidate = new T[length];
				}
				else
				{
					if (zerofill)
						Array.Clear(candidate, 0, candidate.Length);
					_available.Remove(candidate);
				}
				_inuse.Add(candidate);
				return candidate;
			}

			public T[] GetBuffer(int length, bool zerofill = false)
			{
				return GetBufferInternal(length, zerofill, a => a.Length == length);
			}

			public T[] GetBufferAtLeast(int length, bool zerofill = false)
			{
				return GetBufferInternal(length, zerofill, a => a.Length >= length && a.Length / (float)length <= 2.0f);
			}

			public void ReleaseBuffer(T[] buffer)
			{
				if (!_inuse.Remove(buffer))
					throw new ArgumentException();
				_available.Add(buffer);
			}
		}

		#endregion

		#region binary write helpers

		/// <summary>
		/// variable length value, unsigned
		/// </summary>
		static void WriteVarU(ulong v, Stream stream)
		{
			byte[] b = new byte[10];
			int i = 0;
			do
			{
				if (i > 0)
					b[i++] = (byte)((v & 127) | 128);
				else
					b[i++] = (byte)(v & 127);
				v /= 128;
			} while (v > 0);
			for (; i > 0; i--)
				stream.WriteByte(b[i - 1]);
		}

		/// <summary>
		/// variable length value, unsigned
		/// </summary>
		static void WriteVarU(int v, Stream stream)
		{
			if (v < 0)
				throw new ArgumentOutOfRangeException("v", "unsigned must be non-negative");
			WriteVarU((ulong)v, stream);
		}

		/// <summary>
		/// variable length value, unsigned
		/// </summary>
		static void WriteVarU(long v, Stream stream)
		{
			if (v < 0)
				throw new ArgumentOutOfRangeException("v", "unsigned must be non-negative");
			WriteVarU((ulong)v, stream);
		}

		/// <summary>
		/// variable length value, signed
		/// </summary>
		static void WriteVarS(long v, Stream stream)
		{
			ulong temp;
			if (v < 0)
				temp = 1 + 2 * (ulong)(-v);
			else
				temp = 2 * (ulong)(v);
			WriteVarU(temp - 1, stream);
		}

		/// <summary>
		/// utf-8 string with length prepended
		/// </summary>
		static void WriteString(string s, Stream stream)
		{
			WriteBytes(Encoding.UTF8.GetBytes(s), stream);
		}

		/// <summary>
		/// arbitrary sequence of bytes with length prepended
		/// </summary>
		static void WriteBytes(byte[] b, Stream stream)
		{
			WriteVarU(b.Length, stream);
			stream.Write(b, 0, b.Length);
		}

		/// <summary>
		/// big endian 64 bit unsigned
		/// </summary>
		/// <param name="v"></param>
		/// <param name="stream"></param>
		static void WriteBE64(ulong v, Stream stream)
		{
			byte[] b = new byte[8];
			for (int i = 7; i >= 0; i--)
			{
				b[i] = (byte)(v & 255);
				v >>= 8;
			}
			stream.Write(b, 0, 8);
		}

		/// <summary>
		/// big endian 32 bit unsigned
		/// </summary>
		/// <param name="v"></param>
		/// <param name="stream"></param>
		static void WriteBE32(uint v, Stream stream)
		{
			byte[] b = new byte[4];
			for (int i = 3; i >= 0; i--)
			{
				b[i] = (byte)(v & 255);
				v >>= 8;
			}
			stream.Write(b, 0, 4);
		}

		/// <summary>
		/// big endian 32 bit unsigned
		/// </summary>
		/// <param name="v"></param>
		/// <param name="stream"></param>
		static void WriteBE32(int v, Stream stream)
		{
			byte[] b = new byte[4];
			for (int i = 3; i >= 0; i--)
			{
				b[i] = (byte)(v & 255);
				v >>= 8;
			}
			stream.Write(b, 0, 4);
		}

		#endregion

		#region CRC calculator

		static readonly uint[] CRCtable = new uint[]
		{
			0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9,
			0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005,
			0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61,
			0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD,
		};

		/// <summary>
		/// seems to be different than standard CRC32?????
		/// </summary>
		/// <param name="buf"></param>
		/// <returns>crc32, nut variant</returns>
		static uint NutCRC32(byte[] buf)
		{
			uint crc = 0;
			for (int i = 0; i < buf.Length; i++)
			{
				crc ^= (uint)buf[i] << 24;
				crc = (crc << 4) ^ CRCtable[crc >> 28];
				crc = (crc << 4) ^ CRCtable[crc >> 28];
			}
			return crc;
		}

		#endregion

		/// <summary>
		/// writes a single packet out, including checksums
		/// </summary>
		class NutPacket : Stream
		{
			public enum StartCode : ulong
			{
				Main = 0x4e4d7a561f5f04ad,
				Stream = 0x4e5311405bf2f9db,
				Syncpoint = 0x4e4be4adeeca4569,
				Index = 0x4e58dd672f23e64e,
				Info = 0x4e49ab68b596ba78
			};

			MemoryStream data;
			StartCode startcode;
			Stream underlying;

			/// <summary>
			/// create a new NutPacket
			/// </summary>
			/// <param name="startcode">startcode for this packet</param>
			/// <param name="underlying">stream to write to</param>
			public NutPacket(StartCode startcode, Stream underlying)
			{
				data = new MemoryStream();
				this.startcode = startcode;
				this.underlying = underlying;
			}


			public override bool CanRead
			{
				get { return false; }
			}

			public override bool CanSeek
			{
				get { return false; }
			}

			public override bool CanWrite
			{
				get { return true; }
			}

			/// <summary>
			/// write data out to underlying stream, including header, footer, checksums
			/// this cannot be done more than once!
			/// </summary>
			public override void Flush()
			{
				// first, prep header
				var header = new MemoryStream();
				WriteBE64((ulong)startcode, header);
				WriteVarU(data.Length + 4, header); // +4 for checksum
				if (data.Length > 4092)
					WriteBE32(NutCRC32(header.ToArray()), header);
				var tmp = header.ToArray();
				underlying.Write(tmp, 0, tmp.Length);

				tmp = data.ToArray();
				underlying.Write(tmp, 0, tmp.Length);
				WriteBE32(NutCRC32(tmp), underlying);

				data = null;
			}
			
			public override long Length
			{
				get { throw new NotImplementedException(); }
			}

			public override long Position
			{
				get
				{
					throw new NotImplementedException();
				}
				set
				{
					throw new NotImplementedException();
				}
			}

			public override int Read(byte[] buffer, int offset, int count)
			{
				throw new NotImplementedException();
			}

			public override long Seek(long offset, SeekOrigin origin)
			{
				throw new NotImplementedException();
			}

			public override void SetLength(long value)
			{
				throw new NotImplementedException();
			}
			
			public override void Write(byte[] buffer, int offset, int count)
			{
				data.Write(buffer, offset, count);
			}
		}

		#region fields

		/// <summary>
		/// stores basic AV parameters
		/// </summary>
		class AVParams
		{
			public int width, height, samplerate, fpsnum, fpsden, channels;
			/// <summary>
			/// puts fpsnum, fpsden in lowest terms
			/// </summary>
			public void Reduce()
			{
				int gcd = (int) BigInteger.GreatestCommonDivisor(new BigInteger(fpsnum), new BigInteger(fpsden));
				fpsnum /= gcd;
				fpsden /= gcd;
			}
		}

		/// <summary>
		/// stores basic AV parameters
		/// </summary>
		AVParams avparams;

		/// <summary>
		/// target output for nut stream
		/// </summary>
		Stream output;

		/// <summary>
		/// PTS of video stream.  timebase is 1/framerate, so this is equal to number of frames
		/// </summary>
		ulong videopts;

		/// <summary>
		/// PTS of audio stream.  timebase is 1/samplerate, so this is equal to number of samples
		/// </summary>
		ulong audiopts;

		/// <summary>
		/// has EOR been writen on this stream?
		/// </summary>
		bool videodone;
		/// <summary>
		/// has EOR been written on this stream?
		/// </summary>
		bool audiodone;

		/// <summary>
		/// video packets waiting to be written
		/// </summary>
		Queue<NutFrame> videoqueue;
		/// <summary>
		/// audio packets waiting to be written
		/// </summary>
		Queue<NutFrame> audioqueue;

		ReusableBufferPool<byte> _bufferpool = new ReusableBufferPool<byte>(12);

		#endregion

		#region header writers

		/// <summary>
		/// write out the main header
		/// </summary>
		void writemainheader()
		{
			// note: this file starttag not actually part of main headers
			var tmp = Encoding.ASCII.GetBytes("nut/multimedia container\0");
			output.Write(tmp, 0, tmp.Length);

			var header = new NutPacket(NutPacket.StartCode.Main, output);

			WriteVarU(3, header); // version
			WriteVarU(2, header); // stream_count
			WriteVarU(65536, header); // max_distance

			WriteVarU(2, header); // time_base_count
			// timebase is length of single frame, so reversed num+den is intentional
			WriteVarU(avparams.fpsden, header); // time_base_num[0]
			WriteVarU(avparams.fpsnum, header); // time_base_den[0]
			WriteVarU(1, header); // time_base_num[1]
			WriteVarU(avparams.samplerate, header); // time_base_den[1]

			// frame flag compression is ignored for simplicity
			for (int i = 0; i < 255; i++) // not 256 because entry 0x4e is skipped (as it would indicate a startcode)
			{
				WriteVarU((1 << 12), header); // tmp_flag = FLAG_CODED
				WriteVarU(0, header); // tmp_fields
			}

			// header compression ignored because it's not useful to us
			WriteVarU(0, header); // header_count_minus1

			// BROADCAST_MODE only useful for realtime transmission clock recovery
			WriteVarU(0, header); // main_flags

			header.Flush();
		}

		/// <summary>
		/// write out the 0th stream header (video)
		/// </summary>
		void writevideoheader()
		{
			var header = new NutPacket(NutPacket.StartCode.Stream, output);
			WriteVarU(0, header); // stream_id
			WriteVarU(0, header); // stream_class = video
			WriteString("BGRA", header); // fourcc = "BGRA"
			WriteVarU(0, header); // time_base_id = 0
			WriteVarU(8, header); // msb_pts_shift
			WriteVarU(1, header); // max_pts_distance
			WriteVarU(0, header); // decode_delay
			WriteVarU(1, header); // stream_flags = FLAG_FIXED_FPS
			WriteBytes(new byte[0], header); // codec_specific_data

			// stream_class = video
			WriteVarU(avparams.width, header); // width
			WriteVarU(avparams.height, header); // height
			WriteVarU(1, header); // sample_width
			WriteVarU(1, header); // sample_height
			WriteVarU(18, header); // colorspace_type = full range rec709 (avisynth's "PC.709")

			header.Flush();
		}

		/// <summary>
		/// write out the 1st stream header (audio)
		/// </summary>
		void writeaudioheader()
		{
			var header = new NutPacket(NutPacket.StartCode.Stream, output);
			WriteVarU(1, header); // stream_id
			WriteVarU(1, header); // stream_class = audio
			WriteString("\x01\x00\x00\x00", header); // fourcc = 01 00 00 00
			WriteVarU(1, header); // time_base_id = 1
			WriteVarU(8, header); // msb_pts_shift
			WriteVarU(avparams.samplerate, header); // max_pts_distance
			WriteVarU(0, header); // decode_delay
			WriteVarU(0, header); // stream_flags = none; no FIXED_FPS because we aren't guaranteeing same-size audio chunks
			WriteBytes(new byte[0], header); // codec_specific_data

			// stream_class = audio
			WriteVarU(avparams.samplerate, header); // samplerate_num
			WriteVarU(1, header); // samplerate_den
			WriteVarU(avparams.channels, header); // channel_count

			header.Flush();
		}

		#endregion

		/// <summary>
		/// stores a single frame with syncpoint, in mux-ready form
		/// used because reordering of audio and video can be needed for proper interleave
		/// </summary>
		class NutFrame
		{
			/// <summary>
			/// data ready to be written to stream/disk
			/// </summary>
			byte[] data;

			/// <summary>
			/// valid length of the data
			/// </summary>
			int actual_length;

			/// <summary>
			/// presentation timestamp
			/// </summary>
			ulong pts;

			/// <summary>
			/// fraction of the specified timebase
			/// </summary>
			ulong ptsnum, ptsden;

			ReusableBufferPool<byte> _pool;

			/// <summary>
			/// 
			/// </summary>
			/// <param name="payload">frame data</param>
			/// <param name="payloadlen">actual length of frame data</param>
			/// <param name="pts">presentation timestamp</param>
			/// <param name="ptsnum">numerator of timebase</param>
			/// <param name="ptsden">denominator of timebase</param>
			/// <param name="ptsindex">which timestamp base is used, assumed to be also stream number</param>
			public NutFrame(byte[] payload, int payloadlen, ulong pts, ulong ptsnum, ulong ptsden, int ptsindex, ReusableBufferPool<byte> pool)
			{
				this.pts = pts;
				this.ptsnum = ptsnum;
				this.ptsden = ptsden;

				this._pool = pool;
				data = pool.GetBufferAtLeast(payloadlen + 2048);
				var frame = new MemoryStream(data);

				// create syncpoint
				var sync = new NutPacket(NutPacket.StartCode.Syncpoint, frame);
				WriteVarU(pts * 2 + (ulong)ptsindex, sync); // global_key_pts
				WriteVarU(1, sync); // back_ptr_div_16, this is wrong
				sync.Flush();


				var frameheader = new MemoryStream();
				frameheader.WriteByte(0); // frame_code
				// frame_flags = FLAG_CODED, so:
				int flags = 0;
				flags |= 1 << 0; // FLAG_KEY
				if (payloadlen == 0)
					flags |= 1 << 1; // FLAG_EOR
				flags |= 1 << 3; // FLAG_CODED_PTS
				flags |= 1 << 4; // FLAG_STREAM_ID
				flags |= 1 << 5; // FLAG_SIZE_MSB
				flags |= 1 << 6; // FLAG_CHECKSUM
				WriteVarU(flags, frameheader);
				WriteVarU(ptsindex, frameheader); // stream_id
				WriteVarU(pts + 256, frameheader); // coded_pts = pts + 1 << msb_pts_shift
				WriteVarU(payloadlen, frameheader); // data_size_msb

				var frameheaderarr = frameheader.ToArray();
				frame.Write(frameheaderarr, 0, frameheaderarr.Length);
				WriteBE32(NutCRC32(frameheaderarr), frame); // checksum
				frame.Write(payload, 0, payloadlen);

				actual_length = (int)frame.Position;
			}

			/// <summary>
			/// compare two NutFrames by pts
			/// </summary>
			/// <param name="lhs"></param>
			/// <param name="rhs"></param>
			/// <returns></returns>
			public static bool operator <=(NutFrame lhs, NutFrame rhs)
			{
				BigInteger left = new BigInteger(lhs.pts);
				left = left * lhs.ptsnum * rhs.ptsden;
				BigInteger right = new BigInteger(rhs.pts);
				right = right * rhs.ptsnum * lhs.ptsden;

				return left <= right;
			}
			public static bool operator >=(NutFrame lhs, NutFrame rhs)
			{
				BigInteger left = new BigInteger(lhs.pts);
				left = left * lhs.ptsnum * rhs.ptsden;
				BigInteger right = new BigInteger(rhs.pts);
				right = right * rhs.ptsnum * lhs.ptsden;

				return left >= right;
			}

			/// <summary>
			/// write out frame, with syncpoint and all headers
			/// </summary>
			/// <param name="dest"></param>
			public void WriteData(Stream dest)
			{
				dest.Write(data, 0, actual_length);
				_pool.ReleaseBuffer(data);
				//dbg.WriteLine(string.Format("{0},{1},{2}", pts, ptsnum, ptsden));
			}
		}

		/// <summary>
		/// write a video frame to the stream
		/// </summary>
		/// <param name="data">raw video data; if length 0, write EOR</param>
		public void WriteVideoFrame(int[] video)
		{
			if (videodone)
				throw new InvalidOperationException("Can't write data after end of relevance!");
			if (audioqueue.Count > 5)
				throw new Exception("A\\V Desync?");
			int datalen = video.Length * sizeof(int);
			byte[] data = _bufferpool.GetBufferAtLeast(datalen);
			Buffer.BlockCopy(video, 0, data, 0, datalen);
			if (datalen == 0)
				videodone = true;
			var f = new NutFrame(data, datalen, videopts, (ulong) avparams.fpsden, (ulong) avparams.fpsnum, 0, _bufferpool);
			_bufferpool.ReleaseBuffer(data);
			videopts++;
			videoqueue.Enqueue(f);
			while (audioqueue.Count > 0 && f >= audioqueue.Peek())
				audioqueue.Dequeue().WriteData(output);
		}

		/// <summary>
		/// write an audio frame to the stream
		/// </summary>
		/// <param name="data">raw audio data; if length 0, write EOR</param>
		public void WriteAudioFrame(short[] samples)
		{
			if (audiodone)
				throw new Exception("Can't write audio after end of relevance!");
			if (videoqueue.Count > 5)
				throw new Exception("A\\V Desync?");
			int datalen = samples.Length * sizeof(short);
			byte[] data = _bufferpool.GetBufferAtLeast(datalen);
			Buffer.BlockCopy(samples, 0, data, 0, datalen);
			if (datalen == 0)
				audiodone = true;

			var f = new NutFrame(data, datalen, audiopts, 1, (ulong)avparams.samplerate, 1, _bufferpool);
			_bufferpool.ReleaseBuffer(data);
			audiopts += (ulong)samples.Length / (ulong)avparams.channels;
			audioqueue.Enqueue(f);
			while (videoqueue.Count > 0 && f >= videoqueue.Peek())
				videoqueue.Dequeue().WriteData(output);
		}

		/// <summary>
		/// create a new NutMuxer
		/// </summary>
		/// <param name="width">video width</param>
		/// <param name="height">video height</param>
		/// <param name="fpsnum">fps numerator</param>
		/// <param name="fpsden">fps denominator</param>
		/// <param name="samplerate">audio samplerate</param>
		/// <param name="channels">audio number of channels</param>
		/// <param name="underlying">Stream to write to</param>
		public NutMuxer(int width, int height, int fpsnum, int fpsden, int samplerate, int channels, Stream underlying)
		{
			avparams = new AVParams();
			avparams.width = width;
			avparams.height = height;
			avparams.fpsnum = fpsnum;
			avparams.fpsden = fpsden;
			avparams.Reduce(); // timebases in nut MUST be relatively prime
			avparams.samplerate = samplerate;
			avparams.channels = channels;
			output = underlying;

			audiopts = 0;
			videopts = 0;

			audioqueue = new Queue<NutFrame>();
			videoqueue = new Queue<NutFrame>();

			writemainheader();
			writevideoheader();
			writeaudioheader();

			videodone = false;
			audiodone = false;
		}

		/// <summary>
		/// finish and flush everything
		/// closes underlying stream!!
		/// </summary>
		public void Finish()
		{
			if (!videodone)
				WriteVideoFrame(new int[0]);
			if (!audiodone)
				WriteAudioFrame(new short[0]);

			// flush any remaining queued packets

			while (audioqueue.Count > 0 && videoqueue.Count > 0)
			{
				if (audioqueue.Peek() <= videoqueue.Peek())
					audioqueue.Dequeue().WriteData(output);
				else
					videoqueue.Dequeue().WriteData(output);
			}
			while (audioqueue.Count > 0)
				audioqueue.Dequeue().WriteData(output);
			while (videoqueue.Count > 0)
				videoqueue.Dequeue().WriteData(output);

			output.Close();
			output = null;
		}
	}
}
