#region Copyright & License Information
/*
 * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using OpenRA.Graphics;
using OpenRA.Network;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Server
{
	public enum ServerState
	{
		WaitingPlayers = 1,
		GameStarted = 2,
		ShuttingDown = 3
	}
	public class Server
	{
		public readonly string TwoHumansRequiredText = "This server requires at least two human players to start a match.";
		public readonly IPAddress Ip;
		public readonly int Port;
		public readonly MersenneTwister Random = new MersenneTwister();
		public readonly bool Dedicated;
		// Valid player connections
		public List<Connection> Conns = new List<Connection>();
		// Pre-verified player connections
		public List<Connection> PreConns = new List<Connection>();
		public Session LobbyInfo;
		public ServerSettings Settings;
		public ModData ModData;
		public List<string> TempBans = new List<string>();
		// Managed by LobbyCommands
		public MapPreview Map;
		readonly int randomSeed;
		readonly TcpListener listener;
		readonly TypeDictionary serverTraits = new TypeDictionary();
		protected volatile ServerState internalState = ServerState.WaitingPlayers;
		public ServerState State
		{
			get { return internalState; }
			protected set { internalState = value; }
		}
		public static void SyncClientToPlayerReference(Session.Client c, PlayerReference pr)
		{
			if (pr == null)
				return;
			if (pr.LockFaction)
				c.Faction = pr.Faction;
			if (pr.LockSpawn)
				c.SpawnPoint = pr.Spawn;
			if (pr.LockTeam)
				c.Team = pr.Team;
			c.Color = pr.LockColor ? pr.Color : c.PreferredColor;
		}
		static void SendData(Socket s, byte[] data)
		{
			var start = 0;
			var length = data.Length;
			// Non-blocking sends are free to send only part of the data
			while (start < length)
			{
				SocketError error;
				var sent = s.Send(data, start, length - start, SocketFlags.None, out error);
				if (error == SocketError.WouldBlock)
				{
					Log.Write("server", "Non-blocking send of {0} bytes failed. Falling back to blocking send.", length - start);
					s.Blocking = true;
					sent = s.Send(data, start, length - start, SocketFlags.None);
					s.Blocking = false;
				}
				else if (error != SocketError.Success)
					throw new SocketException((int)error);
				start += sent;
			}
		}
		public void Shutdown()
		{
			State = ServerState.ShuttingDown;
		}
		public void EndGame()
		{
			foreach (var t in serverTraits.WithInterface<IEndGame>())
				t.GameEnded(this);
		}
		public Server(IPEndPoint endpoint, ServerSettings settings, ModData modData, bool dedicated)
		{
			Log.AddChannel("server", "server.log");
			listener = new TcpListener(endpoint);
			listener.Start();
			var localEndpoint = (IPEndPoint)listener.LocalEndpoint;
			Ip = localEndpoint.Address;
			Port = localEndpoint.Port;
			Dedicated = dedicated;
			Settings = settings;
			Settings.Name = OpenRA.Settings.SanitizedServerName(Settings.Name);
			ModData = modData;
			randomSeed = (int)DateTime.Now.ToBinary();
			if (UPnP.Status == UPnPStatus.Enabled)
				UPnP.ForwardPort(Settings.ListenPort, Settings.ExternalPort).Wait();
			foreach (var trait in modData.Manifest.ServerTraits)
				serverTraits.Add(modData.ObjectCreator.CreateObject<ServerTrait>(trait));
			LobbyInfo = new Session
			{
				GlobalSettings =
				{
					RandomSeed = randomSeed,
					Map = settings.Map,
					ServerName = settings.Name,
					EnableSingleplayer = settings.EnableSingleplayer || !dedicated,
					GameUid = Guid.NewGuid().ToString()
				}
			};
			new Thread(_ =>
			{
				foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
					t.ServerStarted(this);
				Log.Write("server", "Initial mod: {0}", ModData.Manifest.Id);
				Log.Write("server", "Initial map: {0}", LobbyInfo.GlobalSettings.Map);
				var timeout = serverTraits.WithInterface<ITick>().Min(t => t.TickTimeout);
				for (;;)
				{
					var checkRead = new List<Socket>();
					if (State == ServerState.WaitingPlayers)
						checkRead.Add(listener.Server);
					checkRead.AddRange(Conns.Select(c => c.Socket));
					checkRead.AddRange(PreConns.Select(c => c.Socket));
					if (checkRead.Count > 0)
						Socket.Select(checkRead, null, null, timeout);
					if (State == ServerState.ShuttingDown)
					{
						EndGame();
						break;
					}
					foreach (var s in checkRead)
					{
						if (s == listener.Server)
						{
							AcceptConnection();
							continue;
						}
						var preConn = PreConns.SingleOrDefault(c => c.Socket == s);
						if (preConn != null)
						{
							preConn.ReadData(this);
							continue;
						}
						var conn = Conns.SingleOrDefault(c => c.Socket == s);
						if (conn != null)
							conn.ReadData(this);
					}
					foreach (var t in serverTraits.WithInterface<ITick>())
						t.Tick(this);
					if (State == ServerState.ShuttingDown)
					{
						EndGame();
						if (UPnP.Status == UPnPStatus.Enabled)
							UPnP.RemovePortForward().Wait();
						break;
					}
				}
				foreach (var t in serverTraits.WithInterface<INotifyServerShutdown>())
					t.ServerShutdown(this);
				PreConns.Clear();
				Conns.Clear();
				try { listener.Stop(); }
				catch { }
			}) { IsBackground = true }.Start();
		}
		int nextPlayerIndex;
		public int ChooseFreePlayerIndex()
		{
			return nextPlayerIndex++;
		}
		void AcceptConnection()
		{
			Socket newSocket;
			try
			{
				if (!listener.Server.IsBound)
					return;
				newSocket = listener.AcceptSocket();
			}
			catch (Exception e)
			{
				/* TODO: Could have an exception here when listener 'goes away' when calling AcceptConnection! */
				/* Alternative would be to use locking but the listener doesn't go away without a reason. */
				Log.Write("server", "Accepting the connection failed.", e);
				return;
			}
			var newConn = new Connection { Socket = newSocket };
			try
			{
				newConn.Socket.Blocking = false;
				newConn.Socket.NoDelay = true;
				// assign the player number.
				newConn.PlayerIndex = ChooseFreePlayerIndex();
				SendData(newConn.Socket, BitConverter.GetBytes(ProtocolVersion.Version));
				SendData(newConn.Socket, BitConverter.GetBytes(newConn.PlayerIndex));
				PreConns.Add(newConn);
				// Dispatch a handshake order
				var request = new HandshakeRequest
				{
					Mod = ModData.Manifest.Id,
					Version = ModData.Manifest.Metadata.Version,
					Map = LobbyInfo.GlobalSettings.Map
				};
				DispatchOrdersToClient(newConn, 0, 0, new ServerOrder("HandshakeRequest", request.Serialize()).Serialize());
			}
			catch (Exception e)
			{
				DropClient(newConn);
				Log.Write("server", "Dropping client {0} because handshake failed: {1}", newConn.PlayerIndex.ToString(CultureInfo.InvariantCulture), e);
			}
		}
		void ValidateClient(Connection newConn, string data)
		{
			try
			{
				if (State == ServerState.GameStarted)
				{
					Log.Write("server", "Rejected connection from {0}; game is already started.",
						newConn.Socket.RemoteEndPoint);
					SendOrderTo(newConn, "ServerError", "The game has already started");
					DropClient(newConn);
					return;
				}
				var handshake = HandshakeResponse.Deserialize(data);
				if (!string.IsNullOrEmpty(Settings.Password) && handshake.Password != Settings.Password)
				{
					var message = string.IsNullOrEmpty(handshake.Password) ? "Server requires a password" : "Incorrect password";


Answer: 					SendOrderTo(newConn, "AuthenticationError", message);