using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;

using EA.FileTalkMessages;

namespace EA.FileTalk
{
	/// <summary>
	/// For now we won't expose this structure to user of the FileTalk API
	/// </summary>
	internal struct FileInfo
	{
		public ulong mFileNameOffset;
		public ulong mFileSize;
		public ulong mFileFlags;
		public ulong mFileCreatedDateTime;
		public ulong mFileModifiedDateTime;
		public ulong mFileAccessTime;
	}

	/// <summary>
	/// FileInfoTransaction is for getting file/directory information on a particular 
	/// file and/or directory of files
	/// </summary>
	public class FileInfoTransaction : Transaction
	{
		private FileInfo[] mFileInfos;
		// We could optionally create a hashtable that uses the offset to uniquely 
		// identify the key of the filename
		private string[] mFileNames;

		/// <summary>
		/// This has internal access - since we only intend FileTalkManager to instantiate
		/// one of these
		/// </summary>
		/// <param name="path"></param>
		/// <param name="fileTalkCallback"></param>
		/// <param name="transactionUID"></param>
		/// <param name="gameTalkInstance"></param>
		internal FileInfoTransaction(FileTalkManager.MessageReceivedDelegate fileTalkCallback, 
			uint transactionUID, EA.GameTalk.GameTalkInstance gameTalkInstance, string path)
			: base(fileTalkCallback, gameTalkInstance, transactionUID)
		{
			// The base Transaction MUST register the user's callback before we request
			// the file listing from FileTalkServer
			SendFileInfoRequest(path, transactionUID, gameTalkInstance);
		}

		/// <summary>
		/// We need a way to tell transactions to release resources - somewhat like a delete in C++
		/// However, we can't rely on using a destructor - since we need to be able to control
		/// when this release/freeing occurs.  Currently the only benefit to this is to get
		/// FileNotificationTransaction to tell the FileTalkServer to release its listener(s).
		/// In actual fact - the FileTalkServer will receive a message when this client
		/// disconnects - at which point it will clear all listeners for this client anyway.  This
		/// just gives us more granular control.
		/// </summary>
		public override void Dispose() { }

		/// <summary>
		/// Number of FileInfo's in our array
		/// </summary>
		/// <returns></returns>
		public int FileInfosCount()
		{
			return mFileInfos.Length;
		}

		/// <summary>
		/// Get the filename at specified index
		/// </summary>
		/// <param name="fileIndex"></param>
		/// <returns></returns>
		public string GetFileName(uint fileIndex)
		{
			return mFileNames[fileIndex];
		}

		/// <summary>
		/// Get file size at specified index
		/// </summary>
		/// <param name="fileIndex"></param>
		/// <returns></returns>
		public ulong GetFileSize(uint fileIndex)
		{
			return mFileInfos[fileIndex].mFileSize;
		}

		/// <summary>
		/// Get whether the file info at the specified index is a file or directory (false 
		/// means it is a file)
		/// </summary>
		/// <param name="fileIndex"></param>
		/// <returns></returns>
		public bool IsDirectory(uint fileIndex)
		{
			return ((mFileInfos[fileIndex].mFileFlags & (ulong)FileTalkMessage.FileFlags.IsDirectory) != 0);
		}

		/// <summary>
		/// Get whether this file/dir is read-only
		/// </summary>
		/// <param name="fileIndex"></param>
		/// <returns></returns>
		public bool ReadOnly(uint fileIndex)
		{
			return ((mFileInfos[fileIndex].mFileFlags & (ulong)FileTalkMessage.FileFlags.ReadOnly) != 0);
		}

		/// <summary>
		/// Get date the file/dir at specified index was created
		/// </summary>
		/// <param name="fileIndex"></param>
		/// <returns></returns>
		public ulong DateCreated(uint fileIndex)
		{
			return mFileInfos[fileIndex].mFileCreatedDateTime;
		}

		/// <summary>
		/// Get date the file/dir at specified index was last modified
		/// </summary>
		/// <param name="fileIndex"></param>
		/// <returns></returns>
		public ulong DateLastModified(uint fileIndex)
		{
			return mFileInfos[fileIndex].mFileModifiedDateTime;
		}

		/// <summary>
		/// Get date the file/dir at specified index was last accessed 
		/// </summary>
		/// <param name="fileIndex"></param>
		/// <returns></returns>
		public ulong DateLastAccessed(uint fileIndex)
		{
			return mFileInfos[fileIndex].mFileAccessTime;
		}

		/// <summary>
		/// This will generate an array of FileInfo's (given a byte array of properly 
		/// formatted data)
		/// </summary>
		/// <param name="byteArray"></param>
		/// <param name="numOfFileInfos"></param>
		/// <param name="startOfCharData"></param>
		/// <returns></returns>
		private FileInfo[] CreateFileInfoArray(byte[] byteArray, int numOfFileInfos,
			int startOfCharData)
		{
			MemoryStream mStream = new MemoryStream(byteArray);
			BinaryReader binaryReader = new BinaryReader(mStream);
			// skip the first number that tells us the number of FileInfo's
			binaryReader.ReadUInt64();

			FileInfo[] fileInfos = new FileInfo[numOfFileInfos];
			for (int i = 0; i < fileInfos.Length; i++)
			{
				fileInfos[i].mFileNameOffset = binaryReader.ReadUInt64();
				fileInfos[i].mFileSize = binaryReader.ReadUInt64();
				fileInfos[i].mFileFlags = binaryReader.ReadUInt64();
				fileInfos[i].mFileCreatedDateTime = binaryReader.ReadUInt64();
				fileInfos[i].mFileModifiedDateTime = binaryReader.ReadUInt64();
				fileInfos[i].mFileAccessTime = binaryReader.ReadUInt64();
			}

			// Instead of the following for loop with special case handling for
			// the last element we could iterate byte by byte, looking for the null
			// character and create the strings that way
			mFileNames = new string[fileInfos.Length];
			int byteArrayIndex = 0;
			for (int i = 0; i < fileInfos.Length - 1; i++)
			{
				byteArrayIndex = Convert.ToInt32(fileInfos[i].mFileNameOffset) +
					startOfCharData;
				int lengthOfString = Convert.ToInt32(fileInfos[i + 1].mFileNameOffset) - Convert.ToInt32(fileInfos[i].mFileNameOffset);
				mFileNames[i] = System.Text.ASCIIEncoding.ASCII.GetString(
					byteArray, byteArrayIndex,
					lengthOfString - 1);
			}
			byteArrayIndex = startOfCharData + (int)fileInfos[fileInfos.Length - 1].mFileNameOffset;
			mFileNames[mFileNames.Length - 1] = System.Text.ASCIIEncoding.ASCII.GetString(
					byteArray, byteArrayIndex,
					byteArray.Length - 1 - byteArrayIndex);

			return fileInfos;
		}

		/// <summary>
		/// GameTalk callback registered with this particular transaction's unique 
		/// channel ID (at least unique to this target) - i.e. other targets could
		/// use a transaction registered with the same channel ID
		/// </summary>
		/// <param name="message"></param>
		public override void MessageReceived(EA.GameTalkMessages.GameTalkMessage message)
		{
			// All FileInfoTransaction responses from FileTalkServer must have three parts
			Debug.Assert(message.NumberOfKeys == 3, "FileInfoTransaction received message with invalid content");
			// If I get anything other than a file info response - then I don't understand it
			Debug.Assert(BitConverter.ToInt32(message.GetContentAsBinary(0), 0) == (int)FileTalkMessage.MessageID.FileInfoResponse, "FileInfoTransaction has received a non-FileInfo GameTalkMessage");

			ProcessErrorCode(message.GetContentAsBinary(1));
			if (mErrorOccurred)
			{
				TransactionIsComplete();
				return;
			}

			byte[] responseByteArray = (byte[])message.GetContent(2);
			if (responseByteArray.Length == 0)
			{
				// I'm actually not sure how we couldn't have an error and also have an empty byte array
				mFileInfos = new FileInfo[0];
			}
			else
			{
				// Using all int's below (instead of ulongs or the like) because native methods
				// require ints
				ulong numOfFileInfosUint = BitConverter.ToUInt64(responseByteArray, 0);
				int numOfFileInfos = Convert.ToInt32(numOfFileInfosUint);
				int sizeOfFileInfo = Marshal.SizeOf(typeof(FileInfo));
				int sizeOfFileInfoArray = sizeOfFileInfo * numOfFileInfos;

				// The initial 8 is for the first 8 bytes that make up the numOfFileInfos number
				int startOfCharData = 8 + sizeOfFileInfoArray;

				mFileInfos = CreateFileInfoArray(responseByteArray, numOfFileInfos, startOfCharData);
			}
			TransactionIsComplete();
		}

		/// <summary>
		/// Constructs the GameTalkMessage to be sent to the FileTalkServer
		/// </summary>
		/// <param name="path"></param>
		/// <param name="transactionUID"></param>
		/// <param name="gameTalkInstance"></param>
		private void SendFileInfoRequest(string path, uint transactionUID,
			EA.GameTalk.GameTalkInstance gameTalkInstance)
		{
			EA.GameTalkMessages.GameTalkMessage gameTalkMessage = new EA.GameTalkMessages.GameTalkMessage(mChannelId);
#if(DEBUG)
			// The following sends a string of the specific transaction request being made - shows up in GameTalkMonitor
			gameTalkMessage.AddKeyContentAsBinary(FileTalkMessage.MessageID.FileInfoRequest.ToString(), 
				BitConverter.GetBytes((int)FileTalkMessage.MessageID.FileInfoRequest));
#else
			gameTalkMessage.AddKeyContentAsBinary("", BitConverter.GetBytes((int)FileTalkMessage.MessageID.FileInfoRequest));
#endif
			gameTalkMessage.AddKeyContent(gameTalkInstance.TargetName, path);
			gameTalkInstance.Send(FileTalkMessage.sFileTalkServerID, gameTalkMessage);
		}
	}
}
