using System;
using System.IO;
using System.Collections.Generic;

using BizHawk.Common;

namespace BizHawk.Emulation.DiscSystem
{
	/// <summary>
	/// Parses a RIFF file into a live data structure. 
	/// References to large blobs remain mostly on disk in the file which RiffMaster keeps a reference too. Dispose it to close the file.
	/// You can modify blobs however you want and write the file back out to a new path, if youre careful (that was the original point of this)
	/// Please be sure to test round-tripping when you make any changes. This architecture is a bit tricky to use, but it works if youre careful.
	/// TODO - clarify stream disposing semantics
	/// </summary>
	class RiffMaster : IDisposable
	{
		public RiffMaster() { }

		public void WriteFile(string fname)
		{
			using (FileStream fs = new FileStream(fname, FileMode.Create, FileAccess.Write, FileShare.Read))
			{
				WriteStream(fs);
			}
		}

		public Stream BaseStream;

		public void LoadFile(string fname)
		{
			LoadStream(
				new FileStream(fname, FileMode.Open, FileAccess.Read, FileShare.Read)
			);
		}

		public void Dispose()
		{
			if (BaseStream != null) BaseStream.Dispose();
			BaseStream = null;
		}

		private static string ReadTag(BinaryReader br)
		{
			return "" + br.ReadChar() + br.ReadChar() + br.ReadChar() + br.ReadChar();
		}

		protected static void WriteTag(BinaryWriter bw, string tag)
		{
			for (int i = 0; i < 4; i++)
				bw.Write(tag[i]);
			bw.Flush();
		}

		public abstract class RiffChunk
		{
			public string tag;

			/// <summary>
			/// writes this chunk to the stream, including padding
			/// </summary>
			public abstract void WriteStream(Stream s);

			/// <summary>
			/// distinct from a size or a length, the `volume` is the volume of bytes occupied by the chunk on disk (accounting for padding).
			/// 
			/// </summary>
			public abstract long GetVolume();

			/// <summary>
			/// transforms into a derived class depending on tag
			/// </summary>
			public abstract RiffChunk Morph();
		}

		public class RiffSubchunk : RiffChunk
		{
			public long Position;
			public uint Length;
			public Stream Source;
			public override void WriteStream(Stream s)
			{
				BinaryWriter bw = new BinaryWriter(s);
				WriteTag(bw, tag);
				bw.Write(Length);
				bw.Flush();

				Source.Position = Position;
				Util.CopyStream(Source, s, Length);

				//all chunks are supposed to be 16bit padded
				if (Length % 2 != 0)
					s.WriteByte(0);
			}
			public override long GetVolume()
			{
				long ret = Length;
				if (ret % 2 != 0) ret++;
				return ret;
			}

			public byte[] ReadAll()
			{
				int msSize = (int)Math.Min((long)int.MaxValue, Length);
				MemoryStream ms = new MemoryStream(msSize);
				Source.Position = Position;
				Util.CopyStream(Source, ms, Length);
				return ms.ToArray();
			}

			public override RiffChunk Morph()
			{
				switch (tag)
				{
					case "fmt ": return new RiffSubchunk_fmt(this);
				}
				return this;
			}
		}

		public class RiffSubchunk_fmt : RiffSubchunk
		{
			public enum FORMAT_TAG : ushort
			{
				WAVE_FORMAT_UNKNOWN = (0x0000),
				WAVE_FORMAT_PCM = (0x0001),
				WAVE_FORMAT_ADPCM = (0x0002),
				WAVE_FORMAT_ALAW = (0x0006),
				WAVE_FORMAT_MULAW = (0x0007),
				WAVE_FORMAT_OKI_ADPCM = (0x0010),
				WAVE_FORMAT_DIGISTD = (0x0015),
				WAVE_FORMAT_DIGIFIX = (0x0016),
				IBM_FORMAT_MULAW = (0x0101),
				IBM_FORMAT_ALAW = (0x0102),
				IBM_FORMAT_ADPCM = (0x0103),
			}
			public FORMAT_TAG format_tag;
			public ushort channels;
			public uint samplesPerSec;
			public uint avgBytesPerSec;
			public ushort blockAlign;
			public ushort bitsPerSample;
			public RiffSubchunk_fmt(RiffSubchunk origin)
			{
				tag = "fmt ";
				BinaryReader br = new BinaryReader(new MemoryStream(origin.ReadAll()));
				format_tag = (FORMAT_TAG)br.ReadUInt16();
				channels = br.ReadUInt16();
				samplesPerSec = br.ReadUInt32();
				avgBytesPerSec = br.ReadUInt32();
				blockAlign = br.ReadUInt16();
				bitsPerSample = br.ReadUInt16();
			}
			public override void WriteStream(Stream s)
			{
				Flush();
				base.WriteStream(s);
			}
			void Flush()
			{
				MemoryStream ms = new MemoryStream();
				BinaryWriter bw = new BinaryWriter(ms);
				bw.Write((ushort)format_tag);
				bw.Write(channels);
				bw.Write(samplesPerSec);
				bw.Write(avgBytesPerSec);
				bw.Write(blockAlign);
				bw.Write(bitsPerSample);
				bw.Flush();
				Source = ms;
				Position = 0;
				Length = (uint)ms.Length;
			}

			public override long GetVolume()
			{
				Flush();
				return base.GetVolume();
			}
		}

		public class RiffContainer : RiffChunk
		{
			public RiffChunk GetSubchunk(string tag, string type)
			{
				foreach (RiffChunk rc in subchunks)
					if (rc.tag == tag)
					{
						if (type == null) return rc;
						RiffContainer cont = rc as RiffContainer;
						if (cont != null && cont.type == type)
							return rc;
					}
				return null;
			}

			public RiffContainer()
			{
				tag = "LIST";
			}
			public string type;
			public List<RiffChunk> subchunks = new List<RiffChunk>();
			public override void WriteStream(Stream s)
			{
				BinaryWriter bw = new BinaryWriter(s);
				WriteTag(bw, tag);
				long size = GetVolume();
				if (size > uint.MaxValue) throw new FormatException("File too big to write out");
				bw.Write((uint)size);
				WriteTag(bw, type);
				bw.Flush();
				foreach (RiffChunk rc in subchunks)
					rc.WriteStream(s);
				if (size % 2 != 0)
					s.WriteByte(0);
			}
			public override long GetVolume()
			{
				long len = 4;
				foreach (RiffChunk rc in subchunks)
					len += rc.GetVolume() + 8;
				return len;
			}

			public override RiffChunk Morph()
			{
				switch (type)
				{
					case "INFO": return new RiffContainer_INFO(this);
				}
				return this;
			}
		}

		public class RiffContainer_INFO : RiffContainer
		{
			public Dictionary<string, string> dictionary = new Dictionary<string, string>();
			public RiffContainer_INFO() { type = "INFO"; }
			public RiffContainer_INFO(RiffContainer rc)
			{
				subchunks = rc.subchunks;
				type = "INFO";
				foreach (RiffChunk chunk in subchunks)
				{
					RiffSubchunk rsc = chunk as RiffSubchunk;
					if (chunk == null)
						throw new FormatException("Invalid subchunk of INFO list");
					dictionary[rsc.tag] = System.Text.Encoding.ASCII.GetString(rsc.ReadAll());
				}
			}

			private void Flush()
			{
				subchunks.Clear();
				foreach (KeyValuePair<string, string> kvp in dictionary)
				{
					RiffSubchunk rs = new RiffSubchunk
						{
							tag = kvp.Key,
							Source = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(kvp.Value)),
							Position = 0
						};
					rs.Length = (uint)rs.Source.Length;
					subchunks.Add(rs);
				}
			}

			public override long GetVolume()
			{
				Flush();
				return base.GetVolume();
			}

			public override void WriteStream(Stream s)
			{
				Flush();
				base.WriteStream(s);
			}
		}

		public RiffContainer riff;

		private long readCounter;
		private RiffChunk ReadChunk(BinaryReader br)
		 {
			RiffChunk ret;
			string tag = ReadTag(br); readCounter += 4;
			uint size = br.ReadUInt32(); readCounter += 4;
			if (size > int.MaxValue)
				throw new FormatException("chunk too big");
			if (tag == "RIFF" || tag == "LIST")
			{
				RiffContainer rc = new RiffContainer
					{
						tag = tag,
						type = ReadTag(br)
					};

				readCounter += 4;
				long readEnd = readCounter - 4 + size;
				while (readEnd > readCounter)
					rc.subchunks.Add(ReadChunk(br));
				ret = rc.Morph();
			}
			else
			{
				RiffSubchunk rsc = new RiffSubchunk
					{
						tag = tag,
						Source = br.BaseStream,
						Position = br.BaseStream.Position,
						Length = size
					};
				readCounter += size;
				br.BaseStream.Position += size;
				ret = rsc.Morph();
			}
			if (size % 2 != 0)
			{
				br.ReadByte();
				readCounter += 1;
			}
			return ret;

		}

		public void WriteStream(Stream s)
		{
			riff.WriteStream(s);
		}

		/// <summary>
		/// takes posession of the supplied stream
		/// </summary>
		public void LoadStream(Stream s)
		{
			Dispose();
			BaseStream = s;
			readCounter = 0;
			BinaryReader br = new BinaryReader(s);
			RiffChunk chunk = ReadChunk(br);
			if (chunk.tag != "RIFF") throw new FormatException("can't recognize riff chunk");
			riff = (RiffContainer)chunk;
		}
	}
}