using skyscraper5.DNS.Protocol.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace skyscraper5.DNS.Protocol
{
	public class Domain : IComparable<Domain>
	{
		public static Domain FromArray(byte[] message, int offset, out int endOffset)
		{
			IList<byte[]> labels = new List<byte[]>();
			bool endOffsetAssigned = false;
			endOffset = 0;
			byte lengthOrPointer;
			HashSet<int> visitedOffsetPointers = new HashSet<int>();

			while ((lengthOrPointer = message[offset++]) > 0)
			{
				// Two highest bits are set (pointer)
				if (lengthOrPointer.GetBitValueAt(6, 2) == 3)
				{
					if (!endOffsetAssigned)
					{
						endOffsetAssigned = true;
						endOffset = offset + 1;
					}

					ushort pointer = lengthOrPointer.GetBitValueAt(0, 6);
					offset = (pointer << 8) | message[offset];

					if (visitedOffsetPointers.Contains(offset))
					{
						throw new ArgumentException("Compression pointer loop detected");
					}
					visitedOffsetPointers.Add(offset);

					continue;
				}

				if (lengthOrPointer.GetBitValueAt(6, 2) != 0)
				{
					throw new IndexOutOfRangeException("Unexpected bit pattern in label length");
				}

				byte length = lengthOrPointer;
				byte[] label = new byte[length];
				Array.Copy(message, offset, label, 0, length);

				labels.Add(label);

				offset += length;
			}

			if (!endOffsetAssigned)
			{
				endOffset = offset;
			}

			return new Domain(labels.ToArray());
		}

		public int CompareTo(Domain? other)
		{
			int length = Math.Min(labels.Length, other.labels.Length);

			for (int i = 0; i < length; i++)
			{
				int v = CompareTo(this.labels[i], other.labels[i]);
				if (v != 0) return v;
			}

			return this.labels.Length - other.labels.Length;
		}

		private static int CompareTo(byte[] a, byte[] b)
		{
			int length = Math.Min(a.Length, b.Length);

			for (int i = 0; i < length; i++)
			{
				int v = CompareTo(a[i], b[i]);
				if (v != 0) return v;
			}

			return a.Length - b.Length;
		}

		private static int CompareTo(byte a, byte b)
		{
			if (IsASCIIAlphabet(a) && IsASCIIAlphabet(b))
			{
				a &= ASCII_UPPERCASE_MASK;
				b &= ASCII_UPPERCASE_MASK;
			}

			return a - b;
		}

		private static bool IsASCIIAlphabet(byte b)
		{
			return (ASCII_UPPERCASE_FIRST <= b && b <= ASCII_UPPERCASE_LAST) ||
				(ASCII_LOWERCASE_FIRST <= b && b <= ASCII_LOWERCASE_LAST);
		}

		private const byte ASCII_UPPERCASE_FIRST = 65;
		private const byte ASCII_UPPERCASE_LAST = 90;
		private const byte ASCII_LOWERCASE_FIRST = 97;
		private const byte ASCII_LOWERCASE_LAST = 122;
		private const byte ASCII_UPPERCASE_MASK = 223;

		public Domain(byte[][] labels)
		{
			this.labels = labels;
		}

		private byte[][] labels;

		public int Size
		{
			get { return labels.Sum(l => l.Length) + labels.Length + 1; }
		}

		public static Domain FromArray(byte[] message, int offset)
		{
			return FromArray(message, offset, out offset);
		}

		[DebuggerStepThrough]
		public string ToString(Encoding encoding)
		{
			return string.Join(".", labels.Select(label => encoding.GetString(label)));
		}

		[DebuggerStepThrough]
		public override string ToString()
		{
			return ToString(Encoding.ASCII);
		}
	}
}
