﻿using System;
using System.Linq;
using System.Text;

namespace AppOMatic.Security.Util
{
	public class Arrays
	{
		public static readonly byte[] Empty = new byte[0];
		public static readonly byte[] Zero = { 0 };

		public static byte[] SixtyFourBitLength(byte[] aad)
		{
			return LongToBytes(aad.Length * 8);
		}

		public static byte[] FirstHalf(byte[] arr)
		{
			Ensure.Divisible(arr.Length, 2, "Arrays.FirstHalf() expects even number of element in array.");

			var halfIndex = arr.Length / 2;
			var result = new byte[halfIndex];

			Buffer.BlockCopy(arr, 0, result, 0, halfIndex);

			return result;
		}

		public static byte[] SecondHalf(byte[] arr)
		{
			Ensure.Divisible(arr.Length, 2, "Arrays.SecondHalf() expects even number of element in array.");

			var halfIndex = arr.Length / 2;
			var result = new byte[halfIndex];

			Buffer.BlockCopy(arr, halfIndex, result, 0, halfIndex);

			return result;
		}

		public static byte[] Concat(params byte[][] arrays)
		{
			var result = new byte[arrays.Sum(a => a?.Length ?? 0)];
			var offset = 0;

			foreach(var array in arrays)
			{
				if(array == null) continue;

				Buffer.BlockCopy(array, 0, result, offset, array.Length);
				offset += array.Length;
			}

			return result;
		}

		public static byte[][] Slice(byte[] array, int count)
		{
			Ensure.MinValue(count, 1, "Arrays.Slice() expects count to be above zero, but was {0}", count);
			Ensure.Divisible(array.Length, count, "Arrays.Slice() expects array length to be divisible by {0}", count);

			var sliceCount = array.Length / count;
			var result = new byte[sliceCount][];

			for(var i = 0; i < sliceCount; i++)
			{
				var slice = new byte[count];

				Buffer.BlockCopy(array, i * count, slice, 0, count);

				result[i] = slice;
			}

			return result;
		}

		public static byte[] Xor(byte[] left, long right)
		{
			Ensure.BitSize(left, 64, "Arrays.Xor(byte[], long) expects array size to be 8 bytes, but was {0}", left.Length);

			var bleft = BytesToLong(left);

			return LongToBytes(bleft ^ right);
		}

		public static byte[] Xor(byte[] left, byte[] right)
		{
			Ensure.SameSize(left, right, "Arrays.Xor(byte[], byte[]) expects both arrays to be same legnth, but was given {0} and {1}", left.Length, right.Length);

			var result = new byte[left.Length];

			for(var i = 0; i < left.Length; i++)
			{
				result[i] = (byte)(left[i] ^ right[i]);
			}

			return result;
		}

		public static bool ConstantTimeEquals(byte[] expected, byte[] actual)
		{
			if(expected == actual)
			{
				return true;
			}

			if(expected == null || actual == null)
			{
				return false;
			}

			if(expected.Length != actual.Length)
			{
				return false;
			}

			var equals = true;

			for(var i = 0; i < expected.Length; i++)
			{
				if(expected[i] != actual[i])
				{
					equals = false;
				}
			}

			return equals;
		}

		public static void Dump(byte[] arr, string label = "")
		{
			var builder = new StringBuilder();

			builder.Append(string.Format("{0}({1} bytes): [", label + " ", arr.Length).Trim());

			foreach(var b in arr)
			{
				builder.Append(b);
				builder.Append(",");
			}

			builder.Remove(builder.Length - 1, 1);
			builder.Append("] Hex:[").Append(BitConverter.ToString(arr).Replace("-", " "));
			builder.Append("] Base64Url:").Append(Base64Url.Encode(arr)).Append("\n");
		}

		public static byte[] IntToBytes(int value)
		{
			var uvalue = (uint)value;

			return BitConverter.IsLittleEndian
				? new[] { (byte)((uvalue >> 24) & 0xFF), (byte)((uvalue >> 16) & 0xFF), (byte)((uvalue >> 8) & 0xFF), (byte)(uvalue & 0xFF) }
				: new[] { (byte)(uvalue & 0xFF), (byte)((uvalue >> 8) & 0xFF), (byte)((uvalue >> 16) & 0xFF), (byte)((uvalue >> 24) & 0xFF) };
		}

		public static byte[] LongToBytes(long value)
		{
			var uvalue = (ulong)value;

			return BitConverter.IsLittleEndian
				? new[] { (byte)((uvalue >> 56) & 0xFF), (byte)((uvalue >> 48) & 0xFF), (byte)((uvalue >> 40) & 0xFF), (byte)((uvalue >> 32) & 0xFF), (byte)((uvalue >> 24) & 0xFF), (byte)((uvalue >> 16) & 0xFF), (byte)((uvalue >> 8) & 0xFF), (byte)(uvalue & 0xFF) }
				: new[] { (byte)(uvalue & 0xFF), (byte)((uvalue >> 8) & 0xFF), (byte)((uvalue >> 16) & 0xFF), (byte)((uvalue >> 24) & 0xFF), (byte)((uvalue >> 32) & 0xFF), (byte)((uvalue >> 40) & 0xFF), (byte)((uvalue >> 48) & 0xFF), (byte)((uvalue >> 56) & 0xFF) };
		}

		private static readonly Random _rand = new Random(-DateTime.Now.Millisecond);

		public static byte[] Random(int sizeBits = 128)
		{
			var data = new byte[sizeBits / 8];

			_rand.NextBytes(data);

			return data;
		}


		public static long BytesToLong(byte[] array)
		{
			var msb = BitConverter.IsLittleEndian
						? (long)(array[0] << 24 | array[1] << 16 | array[2] << 8 | array[3]) << 32
						: (long)(array[7] << 24 | array[6] << 16 | array[5] << 8 | array[4]) << 32;

			var lsb = BitConverter.IsLittleEndian
						   ? (array[4] << 24 | array[5] << 16 | array[6] << 8 | array[7]) & 0x00000000ffffffff
						   : (array[3] << 24 | array[2] << 16 | array[1] << 8 | array[0]) & 0x00000000ffffffff;

			return msb | lsb;
		}

		public static byte[] LeftmostBits(byte[] data, int lengthBits)
		{
			Ensure.Divisible(lengthBits, 8, "LeftmostBits() expects length in bits divisible by 8, but was given {0}", lengthBits);

			var byteCount = lengthBits / 8;
			var result = new byte[byteCount];

			Buffer.BlockCopy(data, 0, result, 0, byteCount);

			return result;
		}

		public static byte[] RightmostBits(byte[] data, int lengthBits)
		{
			Ensure.Divisible(lengthBits, 8, "RightmostBits() expects length in bits divisible by 8, but was given {0}", lengthBits);

			var byteCount = lengthBits / 8;
			var result = new byte[byteCount];

			Buffer.BlockCopy(data, data.Length - byteCount, result, 0, byteCount);

			return result;

		}
	}
}
