using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceProcess;
using System.Reflection;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;

using Microsoft.Win32;

using EA.GameTalk;
using EA.FileTalkMessages;

namespace EA.FileTalk
{
	/// <summary>
	/// This is the entry point for ALL users of the FileTalk API
	/// They will use this class to initialize all things FileTalk and subsequently to 
	/// send FileTalk requests to the FileTalkServer
	/// </summary>
	public class FileTalkManager
	{
		/// <summary>
		/// FileTalk Delegate for when a message is received.
		/// </summary>
		public delegate void MessageReceivedDelegate(Transaction transaction);

		/// <summary>
		/// The specific GameTalkInstance used by this instance of FileTalk
		/// </summary>
		private GameTalkInstance mGameTalkInstance;

		/// <summary>
		/// This is incremented for each new transaction created
		/// </summary>
		private uint mTransactionGenerator = 0;
		
		/// <summary>
		/// This is the fragment size used by both GetFileTransactions and SendFileTransactions
		/// </summary>
		private uint mFragmentSize = 1024;

		/// <summary>
		/// This sets how often the FileTalk thread runs (in number of milliseconds)
		/// </summary>
		private int mFileTalkThreadMillis = 5;

		/// <summary>
		/// This FileTalk config callback can be provided by the user so they can 
		/// choose whether they accept a configuration change by a different client
		/// to the FileTalkServer
		/// </summary>
		private GameTalkInstance.MessageReceivedDelegate mFileTalkConfigCallback = null;

		/// <summary>
		/// FileTalkManager uses this container to distribute messages to the applicable 
		/// transactions - the key is the channel id on which the messages are sent between
		/// the transaction and the FileTalkServer
		/// </summary>
		private Dictionary<string, Transaction> mTransactions = new Dictionary<string, Transaction>();

		#region Singleton stuff
		private FileTalkManager()
		{
		}

		/// <summary>
		/// FileTalkManager Singleton
		/// </summary>
		public static FileTalkManager Instance
		{
			get
			{
				return Nested.instance;
			}
		}

		class Nested
		{
			// Explicit static constructor to tell C# compiler
			// not to mark type as beforefieldinit
			static Nested()
			{
			}

			internal static readonly FileTalkManager instance = new FileTalkManager();
		}
		#endregion

		/// <summary>
		/// This method allows C# users to choose when the resources claimed by the FileTalkManager 
		/// are cleared - thus facilitating garbage collection.
		/// </summary>
		public void ReleaseInstance()
		{
			Dictionary<string, Transaction>.ValueCollection.Enumerator transactionEnum = mTransactions.Values.GetEnumerator();
			while (transactionEnum.MoveNext())
			{
				transactionEnum.Current.Dispose();
			}
			mTransactions.Clear();
			mTransactions = null;
		}

		private void SendFileTalkServerEndian()
		{
			EA.GameTalkMessages.GameTalkMessage gameTalkMessage =
				new EA.GameTalkMessages.GameTalkMessage(FileTalkMessage.sFileTalkConfigChannel);
            // The following sends a string of the specific request being made - shows up in GameTalkMonitor
            // We have to send the string for this message since the FileTalkServer doesn't know our endian yet - so it requires the string
            gameTalkMessage.AddKeyContentAsBinary(FileTalkMessage.MessageID.InitMessage.ToString(),
                BitConverter.GetBytes((int)FileTalkMessage.MessageID.InitMessage));
            gameTalkMessage.AddKeyContent(
                mGameTalkInstance.TargetName,
                BitConverter.GetBytes(1), EA.GameTalkMessages.GameTalkMessage.Format.Binary);
			mGameTalkInstance.Send(FileTalkMessage.sFileTalkServerID, gameTalkMessage);
		}

		/// <summary>
		/// Most basic way of initializing FileTalk - just give it a reference to 
		/// GameTalkInstance
		/// </summary>
		/// <param name="gameTalkInstance"></param>
		public void Initialize(GameTalkInstance gameTalkInstance)
		{
			mGameTalkInstance = gameTalkInstance;
			// Since we require GameTalk to have been initiated before FileTalk - both the
			// GameTalkServer and GameTalkMonitor will already have been installed/running by
			// this point.  FileTalk may want to use a more recent version of GameTalk
			// than this client is pointing to.  HOWEVER, FileTalkServer will also initialize
			// GameTalk - thus the following shouldn't be necessary:
			//EA.GameTalk.Installer.Instance.InstallServer();
			//EA.GameTalk.Installer.Instance.InstallViewer();

			EA.FileTalk.Installer.Instance.InstallServer(gameTalkInstance.StartServerAsApp);

			// User may have provided their own config callback - don't overwrite theirs
			if (mFileTalkConfigCallback == null)
				mFileTalkConfigCallback = new EA.GameTalk.GameTalkInstance.MessageReceivedDelegate(ForwardConfigMessage);

			mGameTalkInstance.RegisterMessageHandler(mFileTalkConfigCallback, FileTalkMessages.FileTalkMessage.sFileTalkConfigChannel);

			// This callback will forward all GameTalkMessages to the appropriate transaction
			EA.GameTalk.GameTalkInstance.MessageReceivedDelegate fileTalkTransactionCallback = new EA.GameTalk.GameTalkInstance.MessageReceivedDelegate(TransactionReceived);
			mGameTalkInstance.RegisterMessageHandler(fileTalkTransactionCallback, FileTalkMessages.FileTalkMessage.sFileTalkTransactionChannelPrefix);
			
			// If the FileTalkServer isn't currently running - we will need to buffer our 
			// FileTalk initialization message that includes our specific endian
			mGameTalkInstance.StartBuffering(FileTalkMessage.sFileTalkServerID, 1, false);
			SendFileTalkServerEndian();
		}

		/// <summary>
		/// This Initialize method allows customization of the default fragment size
		/// used in sending data to and from FileTalk.  That is, the user can use
		/// this to increase or decrease the size of the packets sent to and from the
		/// FileTalkServer
		/// </summary>
		/// <param name="gameTalkInstance"></param>
		/// <param name="fragmentSize"></param>
		public void Initialize(GameTalkInstance gameTalkInstance, uint fragmentSize)
		{
			mFragmentSize = fragmentSize;
			Initialize(gameTalkInstance);
		}

		/// <summary>
		/// This Intialize method allows the user to indicate how long the SendFileData
		/// thread should rest/sleep between sends - in addition to setting how large
		/// each fragment of a file should be (that it sends "over the wire").
		/// </summary>
		/// <param name="gameTalkInstance"></param>
		/// <param name="fragmentSize"></param>
		/// <param name="fileTalkThreadMillis"></param>
		public void Initialize(GameTalkInstance gameTalkInstance, uint fragmentSize, 
			int fileTalkThreadMillis)
		{
			mFileTalkThreadMillis = fileTalkThreadMillis;
			Initialize(gameTalkInstance, fragmentSize);
		}

		/// <summary>
		/// This Intialize method allows the user to indicate how long the SendFileData
		/// thread should rest/sleep between sends - in addition to setting how large
		/// each fragment of a file should be (that it sends "over the wire").  We also
		/// give the user the ability to approve or disapprove of FileTalk config's that
		/// a client tries to set its root file path to (through the callback).
		/// </summary>
		/// <param name="gameTalkInstance"></param>
		/// <param name="fragmentSize"></param>
		/// <param name="fileTalkThreadMillis"></param>
		/// <param name="callback"></param>
		public void Initialize(GameTalkInstance gameTalkInstance, uint fragmentSize,
			int fileTalkThreadMillis, EA.GameTalk.GameTalkInstance.MessageReceivedDelegate callback)
		{
			mFileTalkConfigCallback = callback;
			Initialize(gameTalkInstance, fragmentSize, fileTalkThreadMillis);
		}

		void TransactionReceived(EA.GameTalkMessages.GameTalkMessage message)
		{
			Transaction matchingTransaction = null;
			if (mTransactions.TryGetValue(message.ChannelId, out matchingTransaction))
				matchingTransaction.MessageReceived(message);
		}

		/// <summary>
		/// Get a FileInfoTransaction which sends a request for the file information 
		/// on the files contained in a particular directory or for a specific file
		/// </summary>
		/// <param name="filePath"></param>
		/// <param name="callback"></param>
		/// <returns></returns>
		public FileInfoTransaction CreateFileInfoTransaction(string filePath, 
			FileTalkManager.MessageReceivedDelegate callback)
		{
			FileInfoTransaction fileInfoTransaction = new FileInfoTransaction(callback, mTransactionGenerator++, mGameTalkInstance, filePath);
			mTransactions.Add(fileInfoTransaction.GetChannelId(), fileInfoTransaction);
			return fileInfoTransaction;
		}

		/// <summary>
		/// Returns a GetFileTransaction which sends a request for a particular file
		/// </summary>
		/// <param name="filePath"></param>
		/// <param name="callback"></param>
		/// <returns></returns>
		public GetFileTransaction CreateGetFileTransaction(string filePath,
			FileTalkManager.MessageReceivedDelegate callback)
		{
			GetFileTransaction getFileTransaction = new GetFileTransaction(callback, mTransactionGenerator++, mGameTalkInstance, filePath, mFragmentSize, 0, 0);
			mTransactions.Add(getFileTransaction.GetChannelId(), getFileTransaction);
			return getFileTransaction;
		}

		/// <summary>
		/// Returns a GetFileTransaction which sends a request for a particular file
		/// starting at a particular index and for a specified size
		/// </summary>
		/// <param name="filePath"></param>
		/// <param name="callback"></param>
		/// <param name="index"></param>
		/// <param name="numBytes"></param>
		/// <returns></returns>
		public GetFileTransaction CreateGetFileTransaction(string filePath,
			FileTalkManager.MessageReceivedDelegate callback, uint index, uint numBytes)
		{
			GetFileTransaction getFileTransaction = new GetFileTransaction(callback, mTransactionGenerator++, mGameTalkInstance, filePath, mFragmentSize, index, numBytes);
			mTransactions.Add(getFileTransaction.GetChannelId(), getFileTransaction);
			return getFileTransaction;
		}

		/// <summary>
		/// Gets a SendFileTransaction which will send a file from the user's file-system
		/// to the FileTalkServer
		/// </summary>
		/// <param name="filePath"></param>
		/// <param name="fileData"></param>
		/// <param name="overwriteFile"></param>
		/// <param name="callback"></param>
		/// <returns></returns>
		public SendFileTransaction CreateSendFileTransaction(string filePath, byte [] fileData,
			bool overwriteFile, FileTalkManager.MessageReceivedDelegate callback)
		{
			SendFileTransaction sendFileTransaction = new SendFileTransaction(callback, 
				mTransactionGenerator++, mGameTalkInstance, filePath, fileData,
				mFragmentSize, mFileTalkThreadMillis, overwriteFile);
			mTransactions.Add(sendFileTransaction.GetChannelId(), sendFileTransaction);
			return sendFileTransaction;
		}

		/// <summary>
		/// This SendFile overload assumes the user wants to overwrite the file (i.e.
		/// not append to it)
		/// </summary>
		/// <param name="filePath"></param>
		/// <param name="fileData"></param>
		/// <param name="callback"></param>
		/// <returns></returns>
		public SendFileTransaction CreateSendFileTransaction(string filePath, byte[] fileData,
			FileTalkManager.MessageReceivedDelegate callback)
		{
			return CreateSendFileTransaction(filePath, fileData, true, callback);
		}

		/// <summary>
		/// Create a file notification transaction for the user to work with when 
		/// file notifications are received from the FileTalkServer.
		/// </summary>
		/// <param name="filepath"></param>
		/// <param name="filter"></param>
		/// <param name="notifyFilterFlag"></param>
		/// <param name="eventsFlag"></param>
		/// <param name="includeSubdirectories"></param>
		/// <param name="callback"></param>
		/// <returns></returns>
		public FileNotificationTransaction CreateFileNotificationTransaction(string filepath,
			string filter, uint notifyFilterFlag, uint eventsFlag, bool includeSubdirectories,
			FileTalkManager.MessageReceivedDelegate callback)
		{
			FileNotificationTransaction fileNotificationTransaction = new FileNotificationTransaction(callback, mTransactionGenerator++, 
				mGameTalkInstance, filepath, filter, notifyFilterFlag, eventsFlag, includeSubdirectories);
			mTransactions.Add(fileNotificationTransaction.GetChannelId(), fileNotificationTransaction);
			return fileNotificationTransaction;
		}

		/// <summary>
		/// Used to set the file root for all future file paths submitted to FileTalkServer
		/// </summary>
		/// <param name="client"></param>
		/// <param name="fileRoot"></param>
		/// <returns></returns>
		public bool SetFileRootForClient(string client, string fileRoot)
		{
			EA.GameTalkMessages.GameTalkMessage gameTalkMessage = new
				EA.GameTalkMessages.GameTalkMessage(FileTalkMessage.sFileTalkConfigChannel);
			gameTalkMessage.AddKeyContent(FileTalkMessage.MessageID.SetClientFileRoot.ToString(), fileRoot);
			gameTalkMessage.AddKeyContent(mGameTalkInstance.TargetName, string.Empty);
			bool msgForClientSentToGts = mGameTalkInstance.Send(client, gameTalkMessage);

			EA.GameTalkMessages.GameTalkMessage gtmForFileTalkServer = new EA.GameTalkMessages.GameTalkMessage(FileTalkMessage.sFileTalkConfigChannel);
#if(DEBUG)
			// The following sends a string of the specific request being made - shows up in GameTalkMonitor
			gtmForFileTalkServer.AddKeyContentAsBinary(FileTalkMessage.MessageID.SetTentativeFileRoot.ToString(),
				BitConverter.GetBytes((int)FileTalkMessage.MessageID.SetTentativeFileRoot));
#else
			gtmForFileTalkServer.AddKeyContentAsBinary("", BitConverter.GetBytes((int)FileTalkMessage.MessageID.SetTentativeFileRoot));
#endif
			gtmForFileTalkServer.AddKeyContent(mGameTalkInstance.TargetName, fileRoot);
			gtmForFileTalkServer.AddKeyContent(client, "");
			bool sentMsgToFileTalkServer = mGameTalkInstance.Send(FileTalkMessage.sFileTalkServerID, gtmForFileTalkServer);

			return msgForClientSentToGts && sentMsgToFileTalkServer;
		}

		/// <summary>
		/// Used to remove any file root that has been set for the client name passed into this function
		/// </summary>
		/// <param name="client"></param>
		/// <returns></returns>
		public bool RemoveFileRootForClient(string client)
		{
			EA.GameTalkMessages.GameTalkMessage gameTalkMessage = new EA.GameTalkMessages.GameTalkMessage(FileTalkMessage.sFileTalkConfigChannel);
			gameTalkMessage.AddKeyContent(FileTalkMessage.MessageID.RemoveClientFileRoot.ToString(), string.Empty);
			gameTalkMessage.AddKeyContent(mGameTalkInstance.TargetName, string.Empty);
			return mGameTalkInstance.Send(client, gameTalkMessage);
		}

		/// <summary>
		/// Accept the tentative path that has been sent by a different GameTalk client
		/// </summary>
		/// <returns></returns>
		public bool AcceptTentativeFileRoot()
		{
			EA.GameTalkMessages.GameTalkMessage gtmForFileTalkServer = new EA.GameTalkMessages.GameTalkMessage(FileTalkMessage.sFileTalkConfigChannel);
#if(DEBUG)
			// The following sends a string of the specific request being made - shows up in GameTalkMonitor
			gtmForFileTalkServer.AddKeyContentAsBinary(FileTalkMessage.MessageID.AcceptTentativeFileRoot.ToString(),
				BitConverter.GetBytes((int)FileTalkMessage.MessageID.AcceptTentativeFileRoot));
#else
			gtmForFileTalkServer.AddKeyContentAsBinary("", BitConverter.GetBytes((int)FileTalkMessage.MessageID.AcceptTentativeFileRoot));
#endif
			gtmForFileTalkServer.AddKeyContent(mGameTalkInstance.TargetName, "");
			return mGameTalkInstance.Send(FileTalkMessage.sFileTalkServerID, gtmForFileTalkServer);
		}

		/// <summary>
		/// This is the default configuration message forwarding mechanism for FileTalk
		/// If the user doesn't provide their own - then this will simply "consent" to 
		/// any configuration changes and forward them to the FileTalkServer
		/// </summary>
		/// <param name="message"></param>
		public void ForwardConfigMessage(EA.GameTalkMessages.GameTalkMessage message)
		{
			if (message.NumberOfKeys < 2)
				return;

			EA.GameTalkMessages.GameTalkMessage gameTalkMessage = new EA.GameTalkMessages.GameTalkMessage(FileTalkMessage.sFileTalkConfigChannel);
#if(DEBUG)
            // The following sends a string of the specific request being made - shows up in GameTalkMonitor
            gameTalkMessage.AddKeyContentAsBinary(FileTalkMessage.MessageID.SetClientFileRoot.ToString(),
                BitConverter.GetBytes((int)FileTalkMessage.MessageID.SetClientFileRoot));
#else
			gameTalkMessage.AddKeyContentAsBinary("", BitConverter.GetBytes((int)FileTalkMessage.MessageID.SetClientFileRoot));
#endif
            gameTalkMessage.AddKeyContent(mGameTalkInstance.TargetName, message.GetContentAsString(0));
			mGameTalkInstance.Send(FileTalkMessage.sFileTalkServerID, gameTalkMessage);
		}
	}
}
