#region Copyright & License Information
/*
 * Copyright 2007-2021 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.IO;

namespace OpenRA.Network
{
	public class OrderPacket
	{
		readonly Order[] orders;
		readonly MemoryStream data;
		public OrderPacket(Order[] orders)
		{
			this.orders = orders;
			data = null;
		}

		public OrderPacket(MemoryStream data)
		{
			orders = null;
			this.data = data;
		}

		public IEnumerable<Order> GetOrders(World world)
		{
			return orders ?? ParseData(world);
		}

		IEnumerable<Order> ParseData(World world)
		{
			if (data == null)
				yield break;

			// Order deserialization depends on the current world state,
			// so must be deferred until we are ready to consume them.
			var reader = new BinaryReader(data);
			while (data.Position < data.Length)
			{
				var o = Order.Deserialize(world, reader);
				if (o != null)
					yield return o;
			}
		}

		public byte[] Serialize(int frame)
		{
			if (data != null)
				return data.ToArray();

			var ms = new MemoryStream();
			ms.WriteArray(BitConverter.GetBytes(frame));
			foreach (var o in orders)
				ms.WriteArray(o.Serialize());
			return ms.ToArray();
		}

		public static OrderPacket Combine(IEnumerable<OrderPacket> packets)
		{
			var orders = new List<Order>();
			foreach (var packet in packets)
			{
				if (packet.orders == null)
					throw new InvalidOperationException("OrderPacket.Combine can only be used with locally generated OrderPackets.");

				orders.AddRange(packet.orders);
			}

			return new OrderPacket(orders.ToArray());
		}
	}

	public static class OrderIO
	{
		static readonly OrderPacket NoOrders = new OrderPacket(Array.Empty<Order>());

		public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
		{
			var ms = new MemoryStream(4 + Order.SyncHashOrderLength);
			ms.WriteArray(BitConverter.GetBytes(data.Frame));
			ms.WriteByte((byte)OrderType.SyncHash);
			ms.WriteArray(BitConverter.GetBytes(data.SyncHash));
			ms.WriteArray(BitConverter.GetBytes(data.DefeatState));
			return ms.GetBuffer();
		}

		public static byte[] SerializePingResponse(long timestamp, byte queueLength)
		{
			var ms = new MemoryStream(14);
			ms.WriteArray(BitConverter.GetBytes(0));
			ms.WriteByte((byte)OrderType.Ping);
			ms.WriteArray(BitConverter.GetBytes(timestamp));
			ms.WriteByte(queueLength);
			return ms.GetBuffer();
		}

		public static bool TryParseDisconnect((int FromClient, byte[] Data) packet, out (int Frame, int ClientId) disconnect)
		{
			// Valid Disconnect packets are only ever generated by the server
			if (packet.FromClient != 0 || packet.Data.Length != Order.DisconnectOrderLength + 4 || packet.Data[4] != (byte)OrderType.Disconnect)
			{
				disconnect = (0, 0);
				return false;
			}

			var frame = BitConverter.ToInt32(packet.Data, 0);
			var clientId = BitConverter.ToInt32(packet.Data, 5);
			disconnect = (frame, clientId);
			return true;
		}

		public static bool TryParseSync(byte[] packet, out (int Frame, int SyncHash, ulong DefeatState) data)
		{
			if (packet.Length != 4 + Order.SyncHashOrderLength || packet[4] != (byte)OrderType.SyncHash)
			{
				data = (0, 0, 0);
				return false;
			}

			var frame = BitConverter.ToInt32(packet, 0);
			var syncHash = BitConverter.ToInt32(packet, 5);
			var defeatState = BitConverter.ToUInt64(packet, 9);
			data = (frame, syncHash, defeatState);
			return true;
		}

		public static bool TryParseTickScale((int FromClient, byte[] Data) packet, out float scale)
		{
			// Valid tick scale commands are only ever generated by the server
			if (packet.FromClient != 0 || packet.Data.Length != 9 || packet.Data[4] != (byte)OrderType.TickScale)
			{
				scale = 1;
				return false;
			}

			// Valid tick scale packets always have frame 0
			var frame = BitConverter.ToInt32(packet.Data, 0);
			if (frame != 0)
			{
				scale = 1;
				return false;
			}

			scale = BitConverter.ToSingle(packet.Data, 5);
			return true;
		}

		public static bool TryParsePingRequest((int FromClient, byte[] Data) packet, out long timestamp)
		{
			// Valid Ping requests are only ever generated by the server
			if (packet.FromClient != 0 || packet.Data.Length != 13 || packet.Data[4] != (byte)OrderType.Ping)
			{
				timestamp = 0;
				return false;
			}

			// Valid Ping packets always have frame 0
			var frame = BitConverter.ToInt32(packet.Data, 0);
			if (frame != 0)
			{
				timestamp = 0;
				return false;
			}

			timestamp = BitConverter.ToInt64(packet.Data, 5);
			return true;
		}

		public static bool TryParseAck((int FromClient, byte[] Data) packet, out int frame, out byte count)
		{
			// Ack packets are only accepted from the server
			if (packet.FromClient != 0 || packet.Data.Length != 6 || packet.Data[4] != (byte)OrderType.Ack)
			{
				frame = count = 0;
				return false;
			}

			frame = BitConverter.ToInt32(packet.Data, 0);
			count = packet.Data[5];
			return true;
		}

		public static bool TryParseOrderPacket(byte[] packet, out (int Frame, OrderPacket Orders) data)
		{
			// Not a valid packet
			if (packet.Length < 4)
			{
				data = (0, null);
				return false;
			}

			// Wrong packet type
			if (packet.Length >= 5 && (packet[4] == (byte)OrderType.Disconnect || packet[4] == (byte)OrderType.SyncHash))
			{
				data = (0, null);
				return false;
			}

			var frame = BitConverter.ToInt32(packet, 0);

			// PERF: Skip empty order frames, often per client each frame
			var orders = packet.Length > 4 ? new OrderPacket(new MemoryStream(packet, 4, packet.Length - 4)) : NoOrders;
			data = (frame, orders);
			return true;
		}
	}
}
