using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Runtime.InteropServices;

namespace NWN.FileTypes
{
	/// <summary>
	/// This class represents a tlk file.  It contains all of the functionality necessary to
	/// merge tlk files.
	/// </summary>
	public class Tlk
	{
		#region public nested classes
		/// <summary>
		/// Flags for the ResRef data.
		/// </summary>
		[Flags] public enum ResRefFlags: int
		{
			None				= 0x0000,
			TextPresent			= 0x0001,
			SoundPresent		= 0x0002,
			SoundLengthPresent	= 0x0004,
		}

		/// <summary>
		/// This class defines an entry in the tlk file.  It contains all of the data for
		/// the entry.
		/// </summary>
		public class TlkEntry
		{
			#region public properties/methods
			/// <summary>
			/// Gets/sets the entrie's flags
			/// </summary>
			public ResRefFlags Flags { get { return flags; } set { flags = value; } }

			/// <summary>
			/// Gets/sets the sound ResRef
			/// </summary>
			public string SoundResRef
			{
				get { return soundResRef; } 
				set
				{
					soundResRef = value;
					if (string.Empty != soundResRef)
						flags |= ResRefFlags.SoundPresent;
					else
						flags &= ~ResRefFlags.SoundPresent;
				}
			}

			/// <summary>
			/// Gets/sets the volume variance.
			/// </summary>
			public int VolumnVariance { get { return volumeVariance; } set { volumeVariance = value; } }

			/// <summary>
			/// Gets/sets the pitch variance.
			/// </summary>
			public int PitchVariance { get { return pitchVariance; } set { pitchVariance = value; } }

			/// <summary>
			/// Gets/sets the sound length.
			/// </summary>
			public float SoundLength
			{
				get { return soundLength; } 
				set
				{
					soundLength = value;
					if (0 != soundLength)
						flags |= ResRefFlags.SoundLengthPresent;
					else
						flags &= ~ResRefFlags.SoundLengthPresent;
				}
			}

			/// <summary>
			/// Gets/sets the entry text.
			/// </summary>
			public string Text
			{ 
				get { return text; } 
				set
				{
					text = value;
					if (string.Empty != text)
						flags |= ResRefFlags.TextPresent;
					else
						flags &= ~ResRefFlags.TextPresent;
				}
			}

			/// <summary>
			/// Returns true if the tlk entry is empty.
			/// </summary>
			public bool IsEmpty { get { return flags == ResRefFlags.None; } }

			/// <summary>
			/// Default constructor
			/// </summary>
			public TlkEntry()
			{
				flags = ResRefFlags.None;
				volumeVariance = 0;
				pitchVariance = 0;
				soundLength = 0;
				soundResRef = string.Empty;;
				this.text = string.Empty;
			}

			/// <summary>
			/// Constuctor to create an entry for a string.
			/// </summary>
			/// <param name="text">The text.</param>
			public TlkEntry(string text)
			{
				flags = ResRefFlags.TextPresent;
				volumeVariance = 0;
				pitchVariance = 0;
				soundLength = 0;
				soundResRef = string.Empty;
				this.text = text;
			}
			#endregion

			#region private fields/properties/methods
			private ResRefFlags flags;
			public int volumeVariance;
			public int pitchVariance;
			public float soundLength;
			public string soundResRef;
			public string text;
			#endregion
		}
		#endregion

		#region public properties
		/// <summary>
		/// Gets the tlk key to be used in dictionary lookups, this is a lower case version
		/// of the name.
		/// </summary>
		public string Key { get { return name.ToLower(); } }

		/// <summary>
		/// Gets the name of the tlk file, the name does not include any path information.
		/// </summary>
		public string Name { get { return name; } }

		/// <summary>
		/// Gets the number of entries in the tlk file.
		/// </summary>
		public int Count { get { return header.stringCount; } }

		/// <summary>
		/// Does a lookup in the tlk file, returning the entry for the specified index.
		/// </summary>
		public TlkEntry this[int index]
		{
			get
			{
				// If the index is out of range return null.
				if (index >= header.stringCount || index < 0) return null;

				// Create a TlkEntry for the entry
				TlkEntry entry = new TlkEntry();
				entry.PitchVariance = resRefs[index].pitchVariance;
				entry.SoundLength = resRefs[index].soundLength;
				entry.VolumnVariance = resRefs[index].volumeVariance;
				entry.SoundResRef = resRefs[index].soundResRef;
				entry.Text = strings[index];
				entry.Flags = (ResRefFlags) resRefs[index].flags;

				// Return the created entry.
				return entry;
			}
			set
			{
				// Make sure the index is within range.
				if (index >= header.stringCount || index < 0) throw new IndexOutOfRangeException();

				resRefs[index].pitchVariance = value.PitchVariance;
				resRefs[index].soundLength = value.SoundLength;
				resRefs[index].volumeVariance = value.VolumnVariance;
				resRefs[index].soundResRef = value.SoundResRef;
				resRefs[index].flags = (int) value.Flags;
				strings[index] = value.Text;
			}
		}
		#endregion

		#region public methods
		/// <summary>
		/// Default constructor.
		/// </summary>
		/// <param name="count">The initial number of entries in the tlk file.</param>
		public Tlk(int count)
		{
			name = string.Empty;
			header = new TlkHeader();
			resRefs = new RawResRef[count];
			strings = new string[count];

			header.fileType = tlkFile;
			header.fileVersion = tlkVersion;
			header.stringCount = count;
			header.stringOffset = 0;

			for (int i = 0; i < count; i++)
			{
				resRefs[i] = new RawResRef();
				resRefs[i].flags = (int) ResRefFlags.None;
				resRefs[i].offsetToString = 0;
				resRefs[i].pitchVariance = 0;
				resRefs[i].soundLength = 0;
				resRefs[i].soundResRef = string.Empty;
				resRefs[i].stringSize = 0;
				resRefs[i].volumeVariance = 0;

				strings[i] = string.Empty;
			}
		}

		/// <summary>
		/// Returns true if the index'th entry is empty.
		/// </summary>
		/// <param name="index">The index of the entry to test</param>
		/// <returns>True if the entry is empty false if it is not</returns>
		public bool IsEmpty(int index)
		{
			if (index >= header.stringCount || index < 0) throw new ArgumentOutOfRangeException();
			return (int) ResRefFlags.None == resRefs[index].flags || string.Empty == strings[index];
		}

		/// <summary>
		/// Pads the tlk to have at least the specified number of entries.  If the tlk file
		/// has less entries than what is given, blank entries are inserted to pad.
		/// </summary>
		/// <param name="length">The new number of entries</param>
		public void Pad(int length)
		{
			// If the tlk file is larger than the pad count then do nothing.
			if (header.stringCount >= length) return;

			// Add blank entries to the tlk file to pad.
			RawResRef[] padded = new RawResRef[length];
			resRefs.CopyTo(padded, 0);
			for (int i = resRefs.Length; i < padded.Length; i++)
			{
				padded[i].flags = 0;
				padded[i].offsetToString = 0;
				padded[i].pitchVariance = 0;
				padded[i].stringSize = 0;
				padded[i].volumeVariance = 0;
				padded[i].soundLength = 0.0f;
				padded[i].soundResRef = string.Empty;
			}

			string[] paddedStrings = new string[length];
			paddedStrings.CopyTo(strings, 0);
			for (int i = strings.Length; i < paddedStrings.Length; i++)
				paddedStrings[i] = string.Empty;

			// Save the new RawResRef array and update the number of entries in
			// the header.
			header.stringCount = length;
			resRefs = padded;
			strings = paddedStrings;
		}

		/// <summary>
		/// Saves the tlk file, the tlk file must have been given a name or this will
		/// throw an InvalidOperationException.
		/// </summary>
		public void Save()
		{
			if (string.Empty == name) throw new InvalidOperationException();
			SaveAs(name);
		}

		/// <summary>
		/// Saves the tlk file under the specified file name.
		/// </summary>
		/// <param name="fileName">The name in which to save the file.</param>
		public void SaveAs(string fileName)
		{
			using (FileStream writer = 
				new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
			{
				// Figure out how big of a buffer we need to store all of the string data.
				int count = 0;
				foreach (string s in strings)
					count += s.Length;

				// Copy all of the string data to a byte array.
				int stringDataIndex = 0;
				byte[] stringData = new byte[count];
				for (int i = 0; i < resRefs.Length; i++)
				{
					// Ignore entries w/o strings.
					if (0 == ((int) ResRefFlags.TextPresent & resRefs[i].flags) ||
						string.Empty == strings[i])
					{
						// Blank the string size and offset just in case to keep
						// the tlk file clean.
						resRefs[i].stringSize = 0;
						resRefs[i].offsetToString = 0;
						continue;
					}

					// Copy the string bytes to the byte array.
					for (int j = 0; j < strings[i].Length; j++)
						stringData[stringDataIndex + j] = (byte) strings[i][j];

					// Save the string offset and size in the ResRef structure.
					resRefs[i].offsetToString = stringDataIndex;
					resRefs[i].stringSize = strings[i].Length;

					// Increment the buffer index to the next free byte.
					stringDataIndex += strings[i].Length;
				}

				// Set the offset to the string data in the header and write the header out.
				header.stringOffset = headerSize + (resRefs.Length * RawResRefSize);
				byte[] buffer = RawSerialize(header);
				writer.Write(buffer, 0, buffer.Length);

				// Write all of the ResRef entries out.
				for (int i = 0; i < resRefs.Length; i++)
				{
					buffer = RawSerialize(resRefs[i]);
					writer.Write(buffer, 0, buffer.Length);
				}

				// Write the raw string data out.
				writer.Write(stringData, 0, stringData.Length);
			}

			this.name = fileName;
		}
		#endregion

		#region public static methods
		/// <summary>
		/// Creates a Tlk object for the specified tlk file.
		/// </summary>
		/// <param name="fileName">The tlk file</param>
		/// <returns>A Tlk object representing the tlk file</returns>
		public static Tlk LoadTlk(string fileName)
		{
			// Open the tlk file.
			Tlk tlk = new Tlk();
			using (FileStream reader = 
				new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
			{
				// Save the name of the tlk file.
				FileInfo info = new FileInfo(fileName);
				tlk.name = info.Name;

				// Read the header and decode it.
				byte[] buffer = new byte[Tlk.headerSize];
				if (reader.Read(buffer, 0, buffer.Length) != buffer.Length)
					ThrowException("Tlk file {0} is corrupt", fileName);
				tlk.DeserializeHeader(buffer);

				// Do a reality check on the tlk file.
				if (tlk.header.fileType != tlkFile) 
					ThrowException("{0} is not a tlk file", fileName);
				if (tlk.header.fileVersion != tlkVersion) 
					ThrowException("{0} is an unsupported tlk file", fileName);

				// Read the RawResRef array and decode it.
				int size = tlk.header.stringCount * Tlk.RawResRefSize;
				buffer = new byte[size];
				if (reader.Read(buffer, 0, buffer.Length) != buffer.Length)
					ThrowException("Tlk file {0} is corrupt", fileName);
				tlk.DeserializeRawResRefs(buffer);

				// Read the raw string data.
				buffer = new byte[reader.Length - tlk.header.stringOffset];
				if (reader.Read(buffer, 0, buffer.Length) != buffer.Length)
					ThrowException("Tlk file {0} is corrupt", fileName);

				// Load the strings from the raw bytes into our string array.
				tlk.strings = new string[tlk.header.stringCount];
				for (int i = 0; i < tlk.header.stringCount; i++)
					tlk.strings[i] = tlk.GetStringFromBuffer(buffer, i);
			}

			return tlk;
		}

		/// <summary>
		/// Merges 2 tlk objects, saving the results in the specified tlk file. The merge tlk file
		/// is just added to the end of the source tlk file.  Unlike 2da merging, the merge tlk
		/// does not overwrite any entries in the source tlk.  Tlk entries are not row critical like
		/// 2da rows (since tlk strings are not saved in character files), so exact positioning of
		/// them is not as critical.
		/// </summary>
		/// <param name="source">The source tlk</param>
		/// <param name="merge">The merge tlk</param>
		/// <param name="outFile">The name of the output tlk file</param>
		/// <returns>The offset of the first entry of the merge tlk in the output file.  This offset
		/// can be used to fixup 2da entries that refer to the merge tlk.</returns>
		public static int MergeTlk(Tlk source, Tlk merge, string outFile)
		{
			/*
			// Open the output tlk.
			using (FileStream writer = 
			   new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None))
			{
				// Build a RawResRef array containing both the source and merge RawResRef arrays.  Then
				// loop through all of the merge entries and fixup the string offsets to point
				// past the source data to the merge data, which we will glue on the end of the
				// source data.
				RawResRef[] outResRefs = new RawResRef[source.resRefs.Length + merge.resRefs.Length];
				source.resRefs.CopyTo(outResRefs, 0);
				merge.resRefs.CopyTo(outResRefs, source.resRefs.Length);
				for (int i = source.resRefs.Length; i < outResRefs.Length; i++)
				{
					if (0 != (outResRefs[i].flags & (int) ResRefFlags.textPresent))
						outResRefs[i].offsetToString += source.stringBytes.Length;
				}

				// Build a header with a string count of all of the source + merge strings, then
				// write it out.
				TlkHeader headerOut = source.header;
				headerOut.stringCount += merge.header.stringCount;
				headerOut.stringOffset = headerSize + (outResRefs.Length * RawResRefSize);
				byte[] headerBytes = RawSerialize(headerOut);
				writer.Write(headerBytes, 0, headerBytes.Length);

				// Write the RawResRef data.
				for (int i = 0; i < outResRefs.Length; i++)
				{
					byte[] bytes = RawSerialize(outResRefs[i]);
					writer.Write(bytes, 0, bytes.Length);
				}

				// Write the source and merge string data out to the file.
				writer.Write(source.stringBytes, 0, source.stringBytes.Length);
				writer.Write(merge.stringBytes, 0, merge.stringBytes.Length);

				writer.Flush();
				writer.Close();
			}

			// Return the number of strings in the source tlk.  Since we glued the merge
			// tlk onto the end of the source tlk, this value will be the fixup value we need
			// to correct 2da tlk references.
			return source.header.stringCount;
			*/
			return 0;
		}
		#endregion

		#region private methods
		/// <summary>
		/// Private constructor, instances of this class must
		/// </summary>
		private Tlk()
		{
			header = new TlkHeader();
			resRefs = null;
			strings = null;
		}

		/// <summary>
		/// Gets the index'th string from the raw string buffer.
		/// </summary>
		/// <param name="buffer">The raw string buffer</param>
		/// <param name="index">Index of the string to get</param>
		/// <returns>The index'th string</returns>
		private string GetStringFromBuffer(byte[] buffer, int index)
		{
			// If the index is out of range or the text present flag is not set then
			// return an empty string.
			if (index >= header.stringCount ||
				0 == (resRefs[index].flags & (int) ResRefFlags.TextPresent)) return string.Empty;;

			// Can't find a converter to build a string from a byte array, so we
			// need a local char array to do the dirty work.
			int offset = resRefs[index].offsetToString;
			char[] chars = new char[resRefs[index].stringSize];
			for (int i = 0; i < chars.Length; i++)
				chars[i] = (char) buffer[i + offset];

			return new string(chars);
		}

		/// <summary>
		/// Deserializes the header from the given byte array. 
		/// </summary>
		/// <param name="bytes">The byte array containing the header</param>
		private void DeserializeHeader(byte[] bytes)
		{
			// Alloc a hglobal to store the bytes.
			IntPtr buffer = Marshal.AllocHGlobal(bytes.Length);
			try
			{
				// Copy the data to unprotected memory, then convert it to a TlkHeader
				// structure
				Marshal.Copy(bytes, 0, buffer, bytes.Length);
				object o = Marshal.PtrToStructure(buffer, typeof(TlkHeader));
				header = (TlkHeader) o;
			}
			finally
			{
				// Free the hglobal before exiting.
				Marshal.FreeHGlobal(buffer);
			}
		}

		/// <summary>
		/// Deserializes the RawResRef array from the given byte array.
		/// </summary>
		/// <param name="bytes"></param>
		private void DeserializeRawResRefs(byte[] bytes)
		{
			// Alloc a hglobal to store the bytes.
			IntPtr buffer = Marshal.AllocHGlobal(RawResRefSize);
			try
			{
				// Create a RawResRef array for all of the entries and loop
				// through the array populating it.
				resRefs = new RawResRef[header.stringCount];
				for (int i = 0; i < resRefs.Length; i++)
				{
					// Copy the bytes of the i'th RawResRef to unprotected memory
					// and convert it to a RawResRef structure.
					Marshal.Copy(bytes, i * RawResRefSize, buffer, RawResRefSize);
					object o = Marshal.PtrToStructure(buffer, typeof(RawResRef));
					resRefs[i] = (RawResRef) o;
				}
			}
			finally
			{
				// Free the hglobal before exiting.
				Marshal.FreeHGlobal(buffer);
			}
		}
		#endregion

		#region private static methods
		/// <summary>
		/// Throws an NWNException exception
		/// </summary>
		/// <param name="format">The message format string</param>
		/// <param name="args">Message arguments</param>
		private static void ThrowException(string format, params object[] args)
		{
			throw new NWNException(format, args);
		}

		/// <summary>
		/// This method serializes an arbitrary object to a byte array for storage in
		/// a tlk file.  The object should have it's data mapped to proper positions
		/// or the serialization won't work.
		/// </summary>
		/// <param name="o">The object to serialize</param>
		/// <returns></returns>
		private static byte[] RawSerialize(object o)
		{
			// Allocate a hglobal to store the object's data.
			int rawsize = Marshal.SizeOf(o);
			IntPtr buffer = Marshal.AllocHGlobal(rawsize);
			try
			{
				// Copy the object to unprotected memory, then copy that to a byte array.
				Marshal.StructureToPtr(o, buffer, false);
				byte[] rawdata = new byte[rawsize];
				Marshal.Copy(buffer, rawdata, 0, rawsize);
				return rawdata;
			}
			finally
			{
				// Free the hglobal before exiting
				Marshal.FreeHGlobal(buffer);
			}
		}
		#endregion

		#region private nested structures/classes
		/// <summary>
		/// Structure to store the header of the tlk file.  This is mapped directly over the
		/// bytes loaded from the tlk file, so we need to declare the mapping explicitly.
		/// Using LayoutKind.Sequential should work, but I had problems with it so used
		/// Explicit to force the issue.  fileType and fileVersions are really strings, but
		/// .NET insists on null terminating strings, and these strings are not null terminated.
		/// They are both 4 bytes so using Int32 works.
		/// </summary>
		[StructLayout(LayoutKind.Explicit, Pack=1, CharSet=CharSet.Ansi)] private struct TlkHeader
		{
			[FieldOffsetAttribute(0)] public Int32 fileType;
			[FieldOffsetAttribute(4)] public Int32 fileVersion;
			[FieldOffsetAttribute(8)] public Int32 language;
			[FieldOffsetAttribute(12)] public Int32 stringCount;
			[FieldOffsetAttribute(16)] public Int32 stringOffset;
		}

		/// <summary>
		/// Structure to represent a RawResRef entry in a tlk file.  This is mapped directly over the
		/// bytes loaded from the tlk file, so we need to declare the mapping explicitly.
		/// </summary>
		[StructLayout(LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)] private struct RawResRef
		{
			public Int32 flags;
			[MarshalAs(UnmanagedType.ByValTStr, SizeConst=16)] public String soundResRef;
			public Int32 volumeVariance;
			public Int32 pitchVariance;
			public Int32 offsetToString;
			public Int32 stringSize;
			public float soundLength;
		}
		#endregion

		#region private fields
		private const int headerSize = 20;
		private const int RawResRefSize = 40;
		private const Int32 tlkFile = 0x204b4c54;
		private const Int32 tlkVersion = 0x302e3356;

		private string name;
		private TlkHeader header;
		private RawResRef[] resRefs;
		private string[] strings;
		#endregion
	}
}