// (c) 2005 Electronic Arts Inc.
//

using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Diagnostics;
using System.Collections.Generic;

using Microsoft.Win32;

using EA.GameTalkMessages;
using EA.FileTalkMessages;
using EA.GameTalk;

namespace EA.FileTalk
{
	public delegate ClientContext GetClientContextDelegate(string clientName);
	/// <summary>
	/// This is the engine behind the FileTalkServer - it is an instance of this class
	/// that will register callbacks with the GameTalkManager and generate responses
	/// for any incoming FileTalk requests, then send those responses back out in 
	/// GameTalkMessages
	/// </summary>
	public class FileServer
	{
		/// <summary>
		/// This thread processes Messages as they are added to the queue potentially forwarding
		/// them to the appropriate ClientContext or just handling them itself.  Also, it will
		/// shut down the FileTalkServer gracefully if it loses its connection with the GameTalkServer
		/// </summary>
		private Thread mServerThread = null;

		private bool mRunning = false;
		private GameTalkManager.MessageReceivedDelegate mFileTalkCallback;
		private GameTalkManager.MessageReceivedDelegate mServerMsgCallback;

		/// <summary>
		/// Container of ClientContexts that are used to handle requests from the various
		/// clients that connect
		/// </summary>
		private Dictionary<string, ClientContext> mClientContexts = new Dictionary<string, ClientContext>();

		/// <summary>
		/// Collection of all ClientContext threads - tracked only so they can be stopped when 
		/// the FileTalkServer is shut-down
		/// </summary>
		private List<Thread> mClientContextThreads = new List<Thread>();

		private void ConnectToGameTalkServer()
		{
			bool isService = (!EA.GameTalkMessages.RegistrySettings.RunningAsApp());
			// If we want to give the GameTalkServer the platform - pass in "PC":
			GameTalkManager.Instance.Initialize(FileTalkMessage.sFileTalkServerID, Dns.GetHostName(), "PC", isService, EA.GameTalkMessages.RegistrySettings.RunningAsApp());

			mFileTalkCallback = new EA.GameTalk.GameTalkManager.MessageReceivedDelegate(FileTalkMessageReceived);
			// Since all FileTalk messages are put use the FileTalk prefix - the following gets us all FileTalk messages
			GameTalkManager.Instance.RegisterMessageHandler(mFileTalkCallback, FileTalkMessage.sFileTalkChannelPrefix);

			mServerMsgCallback = new EA.GameTalk.GameTalkManager.MessageReceivedDelegate(ServerMessageReceived);
			// We need to know when a client disconnects - so we register on the GameTalkServer channel too
			GameTalkManager.Instance.RegisterMessageHandler(mServerMsgCallback, GameTalkMessage.sServerMessageID);
		}

		public FileServer()
		{
			ConnectToGameTalkServer();
			// The following is code that can be used to create and remove logical drives
            /*if (!Directory.Exists("X:\\"))
            {
                //net use x: \\D1015332\c$\tim\GameTalk
                ProcessStartInfo info = new ProcessStartInfo();
                info.FileName = "net";
                info.Arguments = @"use x: \\D1015332\c$\tim\GameTalk";
                info.WorkingDirectory = @"C:\WINDOWS\system32";
                info.WindowStyle = ProcessWindowStyle.Hidden;
                // The four commented out lines are useful for debugging
                info.RedirectStandardOutput = true;
                info.UseShellExecute = false;
                Process p = Process.Start(info);
                string output2 = p.StandardOutput.ReadToEnd();
                Trace.WriteLine("output: " + output2);
                p.WaitForExit();
            }
            else
            {
                //net use x: /delete
                ProcessStartInfo info = new ProcessStartInfo();
                info.FileName = "net";
                info.Arguments = @"use x: /delete";
                info.WorkingDirectory = @"C:\WINDOWS\system32";
                info.WindowStyle = ProcessWindowStyle.Hidden;
                // The four commented out lines are useful for debugging
                info.RedirectStandardOutput = true;
                info.UseShellExecute = false;
                Process p = Process.Start(info);
                string output2 = p.StandardOutput.ReadToEnd();
                Trace.WriteLine("output: " + output2);
                p.WaitForExit();
            }
            foreach (string logicalDrive in Environment.GetLogicalDrives())
            {
                Trace.WriteLine("2. logicalDrive: " + logicalDrive);
            }*/

        }

		/// <summary>
		/// Destructor.
		/// </summary>
		~FileServer()
		{
			StopServer();
		}

		/// <summary>
		/// If a ClientContext doesn't exist for the specified client name, then one will be created
		/// </summary>
		/// <param name="clientName"></param>
		/// <returns></returns>
		private ClientContext GetClientContext(string clientName)
		{
			ClientContext clientContext = null;
			clientName = clientName.ToLower();
			lock (mClientContexts)
			{
				if (!mClientContexts.TryGetValue(clientName, out clientContext))
				{
					clientContext = new ClientContext(clientName, GetClientContext);
					ThreadStart threadStart = delegate { clientContext.Start(); };
					Thread clientContextThread = new Thread(threadStart);
					mClientContextThreads.Add(clientContextThread);
					clientContextThread.Start();
					mClientContexts.Add(clientName, clientContext);
				}
			}
			return clientContext;
		}

		/// <summary>
		/// FileTalkServer will reply on the same channel that the message was sent on
		/// </summary>
		private void ProcessMessage(GameTalkMessage message)
		{
			try
			{
				Trace.WriteLine("GameTalkMessage: " + message.ChannelId);
				// All requests have at least two parts
				if (message.NumberOfKeys < 2)
					return;

				string target = message.GetKey(1);
				// When the GameTalkServer tells us one of our clients has disconnected - we 
				// stop the ClientContext, tell it to remove all its file notifiers, and 
				// remove the ClientContext from our container of ClientContexts
				if (message.ChannelId == GameTalkMessage.sServerMessageID)
				{
					if (message.GetKey(0) == GameTalkMessage.ClientServerMessages.DisconnectedTargetMessage.ToString())
					{
						lock (mClientContexts)
						{
							ClientContext clientContext = null;
							if (mClientContexts.TryGetValue(target, out clientContext))
							{
								if (clientContext.TentativeFileRoot == null) // Unless there is an easy/elegant alternative - if the TentativeFileRoot has been set we'll just never remove this client context - so the file root doesn't have to constantly be reset
								{
									clientContext.Stop();
									clientContext.DeregisterAndClear();
									mClientContexts.Remove(target);

									// I've noticed the FileTalkServer growing over 2,000 handles
									// Not sure why this is happening - but this should fix the symptom
									// though I don't really know the source of the problem - which would be useful
									System.GC.WaitForPendingFinalizers();
									System.GC.Collect();
								}
								else // Clear all FileSystemWatchers and any file buffers that haven't been written out yet
									clientContext.DeregisterAndClear();
							}
						}
					}
				}
				else
				{
					// This message isn't on the GameTalkServer's private channel
					// thus it is likely a message from one of our clients
					ClientContext clientContext = GetClientContext(target);
					clientContext.AddMessageToQueue(message);
				}
			}
			catch (Exception exception)
			{
				// if we get bad data we should recover gracefully and continue running
				Trace.WriteLine("ProcessMessage: exception: " + exception.Message + Environment.NewLine + exception.StackTrace);
			}
		}

		/// <summary>
		/// GameTalk callback that is registered with GameTalkManager
		/// </summary>
		/// <param name="message"></param>
		private void FileTalkMessageReceived(GameTalkMessage message)
		{
			ProcessMessage(message);
		}

		/// <summary>
		/// GameTalk callback that is registered with GameTalkManager - Server messages are received in this callback - but
		/// then they are routed through the same callback that handles all FileTalk messages
		/// </summary>
		/// <param name="message"></param>
		private void ServerMessageReceived(GameTalkMessage message)
		{
			FileTalkMessageReceived(message);
		}

		/// <summary>
		/// This starts the FileTalkServer thread that keeps checking its queue for newly arrived
		/// GameTalkMessages
		/// </summary>
		public void StartServer()
		{
			mRunning = true;
			// Start the FileTalkServer thread
			mServerThread = new Thread(new ThreadStart(ServerThreadStart));
			mServerThread.Name = "FileTalkServer";
			mServerThread.Start();
		}

		/// <summary>
		/// NOTE: It is CRUCIAL that you stop the server thread before you ShutDown GameTalkManager
		/// </summary>
		public void StopServer()
		{
			if (mRunning)
			{
				mRunning = false;
				try
				{
					// Wait for one second for the the debug thread to stop.
					mServerThread.Join(1000);
					// If still alive; Get rid of the thread.
					if (mServerThread.IsAlive)
					{
						mServerThread.Abort();
					}
				}
				catch (Exception exception)
				{
					Trace.WriteLine("StopServer1: Exception: " + exception.Message + Environment.NewLine + exception.StackTrace);
				}
				mServerThread = null;

				// Don't do the following in case there are multiple targets with
				// the same name as the FileTalkServer - this would effectively remove the 
				// server side channels for them as well.
				/*if (GameTalkManager.Instance.IsConnected)
				{
					GameTalkManager.Instance.UnregisterMessageHandler(mFileTalkCallback);
					GameTalkManager.Instance.UnregisterMessageHandler(mServerMsgCallback);
				}*/

				// Shutdown the GameTalkManager singleton - i.e. disconnect from the GameTalkServer
				// and stop our client threads from running
				try
				{
					GameTalkManager.Instance.Shutdown();
				}
				catch { }

				// Ask each of the ClientContext threads to stop running
				Dictionary<string, ClientContext>.ValueCollection.Enumerator clientContextEnum = mClientContexts.Values.GetEnumerator();
				while (clientContextEnum.MoveNext())
				{
					clientContextEnum.Current.Stop();
				}
				// TELL each of the ClientContext threads to stop running
				for (int i = 0; i < mClientContextThreads.Count; i++)
				{
					try
					{
						mClientContextThreads[i].Join(1000);
						if (mClientContextThreads[i].IsAlive)
							mClientContextThreads[i].Abort();
					}
					catch (Exception exception)
					{
						Trace.WriteLine("StopServer2: Exception: " + exception.Message + Environment.NewLine + exception.StackTrace);
					}
				}
			}
		}

		/// <summary>
		/// This method is here so we can gracefully disconnect when GameTalkServer is shut-down
		/// Also it is handy for run-time breakpoints (check the status of members, etc)
		/// </summary>
		private void ServerThreadStart()
		{
			uint sendPingFrequency = 200;
			uint sendPingCounter = 0;
			while (mRunning)
			{
				try
				{
					System.Threading.Thread.Sleep(5);
					bool isAlivePingSent = true;
					if (++sendPingCounter == sendPingFrequency)
					{
						sendPingCounter = 0;
						isAlivePingSent = GameTalkManager.Instance.SendIsAlivePing();
					}

					if ((!isAlivePingSent) || (!GameTalkManager.Instance.IsConnected))
					{
						Trace.WriteLine("Lost connection with GameTalkServer - shutting down...");
						FileTalkService.ShutDownLogger();
						// I'd prefer to get a reference to the ServiceBase and:
						//ServiceBase.Stop();
						// But the service simply restarts itself using the above
						Environment.Exit(0);

						// There is a very real danger here that automatically attempting 
						// to reconnect to the GameTalkServer could result in the 
						// FileTalkServer starting the GameTalkMonitor/GameTalkViewer
						// This is BAD because when a service starts an application, 
						// the application's GUI will not appear.  Also on a more minor
						// note, the user may simply not want the GameTalkServer to get
						// automatically restarted
						//ConnectToGameTalkServer();
					}
				}
				catch (Exception exception)
				{
					// This is here to avoid "tight-looping" and thereby making FileTalkServer processor intensive when it isn't doing anything useful
					System.Threading.Thread.Sleep(5);
					Trace.WriteLine("ServerThreadStart: Exception: " + exception.Message + Environment.NewLine + exception.StackTrace);
				}
			}
		}
	}
}
