using log4net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using skyscraper8.Ssdp.Schema;

namespace skyscraper8.SimpleServiceDiscoveryProtocol
{
	internal class SsdpClient
	{
		private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name);

		private const string SEARCH_STRING =
			"M-SEARCH * HTTP/1.1\r\n" +
			"HOST:239.255.255.250:1900\r\n" +
			"MAN:\"ssdp:discover\"\r\n" +
			"ST:ssdp:all\r\n" +
			"MX:3\r\n\r\n";

		public static IEnumerable<SsdpDevice> GetSsdpDevices(int timeout)
		{
			NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
			IPAddress[] ipAddresses = Array.ConvertAll(networkInterfaces, x => GetIPAddress(x));
			IPEndPoint[] localEndPoints = Array.ConvertAll(ipAddresses, x => new IPEndPoint(x, 1901));
			IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900);
			Socket[] udpSockets = Array.ConvertAll(localEndPoints,
				x =>
				{
					byte[] addressBytes = x.Address.GetAddressBytes();
					if (addressBytes[0] == 169)
						return null;
					Socket temp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
					temp.Bind(x);
					temp.SendTo(Encoding.UTF8.GetBytes(SEARCH_STRING), SocketFlags.None, remoteEndPoint);
					return temp;
				});

			byte[] buffer = new byte[1600];
			int recvBytes = 0;
			DateTime timeStarted = DateTime.Now;
			while (true)
			{
				bool yielded = false;
				for (int i = 0; i < udpSockets.Length; i++)
				{
					if (udpSockets[i] == null)
						continue;

					Array.Clear(buffer);
					recvBytes = 0;
					if (udpSockets[i].Available > 0)
					{
						recvBytes = udpSockets[i].Receive(buffer, SocketFlags.None);
						if (recvBytes > 0)
						{
							string s = Encoding.UTF8.GetString(buffer, 0, recvBytes);
							//Console.WriteLine(s);
							yield return new SsdpDevice(s);
							yielded = true;
						}
					}
				}

				if (yielded)
					continue;

				Thread.Sleep(1);
				if ((DateTime.Now - timeStarted).TotalMilliseconds >= timeout)
					break;
			}
			yield break;
		}

		private static IPAddress GetIPAddress(NetworkInterface networkInterface)
		{
			IPInterfaceProperties ipProperties = networkInterface.GetIPProperties();
			UnicastIPAddressInformationCollection addresses = ipProperties.UnicastAddresses;
			UnicastIPAddressInformation[] addressArray = addresses.ToArray();
			Array.Sort(addressArray, (a, b) => RateIpAddress(b).CompareTo(RateIpAddress(a)));
			UnicastIPAddressInformation address = addressArray[0];
			return address.Address;
		}

		private static int RateIpAddress(UnicastIPAddressInformation address)
		{
			IPAddress ipAddress = address.Address;
			byte[] addressBytes = ipAddress.GetAddressBytes();
			if (addressBytes.Length == 0x10 && addressBytes[0] == 0xfe && addressBytes[1] == 0x80)
			{
				return 1;	//Link local IPv6 is okay, but we prefer IPv4.
			}

			if (addressBytes.Length == 4 && addressBytes[0] == 169 && addressBytes[1] == 254)
			{
				return 2;	//Auto-Assigned Link-Local IPv4, this is okay, 
			}

			if (addressBytes.Length == 4 && addressBytes[0] == 192 && addressBytes[1] == 168)
			{
				return 3;	//A non-auto-assigned Link-Local. This is what we want.
			}

			if (addressBytes.Length == 4 && addressBytes[0] == 172 && (addressBytes[1] >= 16 && addressBytes[1] <= 31))
			{
				return 3;   //A non-auto-assigned Link-Local. This is what we want.
			}

			if (addressBytes.Length == 4 && addressBytes[0] == 127)
			{
				return Int32.MinValue + 1;	//A loopback address. We don't want these.
			}

			if (addressBytes.Length == 16 && addressBytes[0] == 0 && addressBytes[15] == 1)
			{
				return Int32.MinValue;	//An IPv6 loopback. That's even less wanted.
			}
			logger.WarnFormat("Don't know how to rate IP address", address.Address.ToString());
			return Int32.MinValue;
		}

		public static List<SsdpDevice> GetSsdpDevices(int timeout = 1000, string searchTarget = null, ISsdpCache cache = null)
		{
			List<SsdpDevice> inputList = GetSsdpDevices(timeout).ToList();
			List<SsdpDevice> outputList = new List<SsdpDevice>();
			WebClient webClient = new WebClient();
			foreach (SsdpDevice ssdpDevice in inputList)
			{
				if (!string.IsNullOrEmpty(searchTarget))
				{
					if (!searchTarget.Equals(ssdpDevice.SearchTarget))
					{
						continue;
					}
				}

				skyscraper8.Ssdp.Schema.root ssdpMetadata = null;
				if (cache != null)
				{
					if (cache.SsdpDeviceKnown(ssdpDevice))
					{
						byte[] ssdpMetadataByteArray = cache.SsdpGetMetadata(ssdpDevice);
						ssdpMetadata = UnpackSsdpMetadata(ssdpMetadataByteArray);
					}
				}

				if (ssdpMetadata == null)
				{
					byte[] ssdpMetadataByteArray = webClient.DownloadData(ssdpDevice.Location);
					cache?.SsdpStoreMetadata(ssdpDevice, ssdpMetadataByteArray);
					ssdpMetadata = UnpackSsdpMetadata(ssdpMetadataByteArray);
				}

				ssdpDevice.DeviceMetadata = ssdpMetadata;
				if (ssdpMetadata != null)
				{
					logger.Debug(String.Format("{2} is a {0} {1}", ssdpMetadata.device.manufacturer, ssdpMetadata.device.modelName,ssdpDevice.Server));
				}
				outputList.Add(ssdpDevice);
			}

			return outputList;
		}

		public static SsdpDevice GetFirstSatIpServer(int timeout = 1000, ISsdpCache cache = null)
		{
			List<SsdpDevice> ssdpDevices = GetSsdpDevices(1000, "urn:ses-com:device:SatIPServer:1", cache);
			if (ssdpDevices.Count == 0)
				return null;
			return ssdpDevices.First();
		}

		private static XmlSerializer xmlSerializer;
		private static skyscraper8.Ssdp.Schema.root UnpackSsdpMetadata(byte[] xmlBuffer)
		{
			if (xmlSerializer == null)
				xmlSerializer = new XmlSerializer(typeof(root));

			MemoryStream ms = new MemoryStream(xmlBuffer);
			object deserialize = xmlSerializer.Deserialize(ms);
			ms.Dispose();
			return (skyscraper8.Ssdp.Schema.root)deserialize;
		}

	}
}
