<?php

namespace LowEntryUE4PHP\Classes\Internal;

use LowEntryUE4PHP\Classes\AesKey1D;
use LowEntryUE4PHP\Classes\Internal\Aes\EncryptionAesFastEngine1D;
use LowEntryUE4PHP\LowEntry;


class EncryptionAes
{
	const IV_LENGTH = 4;
	const HASH_LENGTH = 2;
	
	/** @var int[] Byte array */
	private static $pearson = [0x62, 0x06, 0x55, 0x96, 0x24, 0x17, 0x70, 0xA4, 0x87, 0xCF, 0xA9, 0x05, 0x1A, 0x40, 0xA5, 0xDB, 0x3D, 0x14, 0x44, 0x59, 0x82, 0x3F, 0x34, 0x66, 0x18, 0xE5, 0x84, 0xF5, 0x50, 0xD8, 0xC3, 0x73, 0x5A, 0xA8, 0x9C, 0xCB, 0xB1, 0x78, 0x02, 0xBE, 0xBC, 0x07, 0x64, 0xB9, 0xAE, 0xF3, 0xA2, 0x0A, 0xED, 0x12, 0xFD, 0xE1, 0x08, 0xD0, 0xAC, 0xF4, 0xFF, 0x7E, 0x65, 0x4F, 0x91, 0xEB, 0xE4, 0x79, 0x7B, 0xFB, 0x43, 0xFA, 0xA1, 0x00, 0x6B, 0x61, 0xF1, 0x6F, 0xB5, 0x52, 0xF9, 0x21, 0x45, 0x37, 0x3B, 0x99, 0x1D, 0x09, 0xD5, 0xA7, 0x54, 0x5D, 0x1E, 0x2E, 0x5E, 0x4B, 0x97, 0x72, 0x49, 0xDE, 0xC5, 0x60, 0xD2, 0x2D, 0x10, 0xE3, 0xF8, 0xCA, 0x33, 0x98, 0xFC, 0x7D, 0x51, 0xCE, 0xD7, 0xBA, 0x27, 0x9E, 0xB2, 0xBB, 0x83, 0x88, 0x01, 0x31, 0x32, 0x11, 0x8D, 0x5B, 0x2F, 0x81, 0x3C, 0x63, 0x9A, 0x23, 0x56, 0xAB, 0x69, 0x22, 0x26, 0xC8, 0x93, 0x3A, 0x4D, 0x76, 0xAD, 0xF6, 0x4C, 0xFE, 0x85, 0xE8, 0xC4, 0x90, 0xC6, 0x7C, 0x35, 0x04, 0x6C, 0x4A, 0xDF, 0xEA, 0x86, 0xE6, 0x9D, 0x8B, 0xBD, 0xCD, 0xC7, 0x80, 0xB0, 0x13, 0xD3, 0xEC, 0x7F, 0xC0, 0xE7, 0x46, 0xE9, 0x58, 0x92, 0x2C, 0xB7, 0xC9, 0x16, 0x53, 0x0D, 0xD6, 0x74, 0x6D, 0x9F, 0x20, 0x5F, 0xE2, 0x8C, 0xDC, 0x39, 0x0C, 0xDD, 0x1F, 0xD1, 0xB6, 0x8F, 0x5C, 0x95, 0xB8, 0x94, 0x3E, 0x71, 0x41, 0x25, 0x1B, 0x6A, 0xA6, 0x03, 0x0E, 0xCC, 0x48, 0x15, 0x29, 0x38, 0x42, 0x1C, 0xC1, 0x28, 0xD9, 0x19, 0x36, 0xB3, 0x75, 0xEE, 0x57, 0xF0, 0x9B, 0xB4, 0xAA, 0xF2, 0xD4, 0xBF, 0xA3, 0x4E, 0xDA, 0x89, 0xC2, 0xAF, 0x6E, 0x2B, 0x77, 0xE0, 0x47, 0x7A, 0x8E, 0x2A, 0xA0, 0x68, 0x30, 0xF7, 0x67, 0x0F, 0x0B, 0x8A, 0xEF];
	
	
	/**
	 * @param int[] $data Byte array
	 *
	 * @return int[] Byte array
	 */
	private static function generateValidationHash($data)
	{
		$dataLength = \count($data);
		if($dataLength <= 0)
		{
			return [];
		}
		$hh = LowEntry::createArray(static::HASH_LENGTH, 0);
		$hhLength = \count($hh);
		for($j = 0; $j < $hhLength; $j++)
		{
			$h = static::$pearson[(($data[0] + $j) & 0xff) % 256];
			for($i = 1; $i < $dataLength; $i++)
			{
				$h = static::$pearson[($h ^ $data[$i & 0xff]) & 0xff];
			}
			$hh[$j] = $h;
		}
		return $hh;
	}
	
	/**
	 * @param int[] $input Byte array
	 * @param bool  $addedValidationHash
	 *
	 * @return int[] Byte array
	 */
	private static function deletePadding($input, $addedValidationHash)
	{
		$paddingLength = LowEntry::castToByte(($input[\count($input) - 1] & 0x0f));
		$newSize = \count($input) - $paddingLength - 1;
		if($addedValidationHash)
		{
			$newSize -= static::HASH_LENGTH;
		}
		if($newSize <= 0)
		{
			return [];
		}
		
		if($addedValidationHash)
		{
			$output = LowEntry::createArray($newSize, 0);
			LowEntry::systemArrayCopy($input, static::HASH_LENGTH, $output, 0, $newSize);
			return $output;
		}
		else
		{
			return LowEntry::arraysCopyOf($input, $newSize, 0);
		}
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return int[] Byte array
	 */
	private static function expandIv($bytes)
	{
		$expanded = LowEntry::createArray(16, 0);
		
		if((static::IV_LENGTH <= 0) || (\count($bytes) < static::IV_LENGTH))
		{
			return $expanded;
		}
		
		$ivi = 0;
		$expi = 0;
		for($row = 0; $row < 4; $row++)
		{
			for($col = 0; $col < 4; $col++)
			{
				$expanded[$expi] = $bytes[$ivi];
				$expi++;
				$ivi++;
				if($ivi == static::IV_LENGTH)
				{
					$ivi = 0;
				}
			}
			$ivi--;
			if($ivi == -1)
			{
				$ivi += static::IV_LENGTH;
			}
		}
		return $expanded;
	}
	
	/**
	 * @param int[] $key Byte array
	 *
	 * @return int[]|null  Byte array
	 */
	private static function fixKey($key)
	{
		if(!isset($key))
		{
			return null;
		}
		$keyLen = \count($key);
		if($keyLen == 0)
		{
			return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
		}
		if(($keyLen == 16) || ($keyLen == 24) || ($keyLen == 32))
		{
			return $key;
		}

//		$newSize = 0;
		if($keyLen <= 16)
		{
			$newSize = 16;
		}
		elseif($keyLen <= 24)
		{
			$newSize = 24;
		}
		else
		{
			$newSize = 32;
		}
		
		$fixedKey = LowEntry::createArray($newSize, 0);
		$keyi = 0;
		for($i = 0; $i < $newSize; $i++)
		{
			$fixedKey[$i] = $key[$keyi];
			
			$keyi++;
			if($keyi >= $keyLen)
			{
				$keyi = 0;
			}
		}
		return $fixedKey;
	}
	
	
	/**
	 * @param int[]    $in Byte array
	 * @param AesKey1D $key
	 * @param bool     $addValidationHash
	 *
	 * @return int[] Byte array
	 */
	private static function encrypt1D($in, $key, $addValidationHash)
	{
		if(!isset($in) || !isset($key) || ($key->rounds <= 0) || !isset($key->encryptionW))
		{
			return [];
		}
		
		$iv = LowEntry::randomBytes(static::IV_LENGTH);
		
		$hashLength = ($addValidationHash ? static::HASH_LENGTH : 0);
		
		$length = 16 - ((\count($in) + $hashLength) % 16);
		$padding = LowEntry::randomBytes($length);
		$padding[\count($padding) - 1] = LowEntry::castToByte(($length - 1) | ($padding[\count($padding) - 1] << 4));
		
		$tmp = LowEntry::createArray(static::IV_LENGTH + \count($in) + $hashLength + $length, 0);
		$bloc = LowEntry::createArray(16, 0);
		
		LowEntry::systemArrayCopy($iv, 0, $tmp, 0, static::IV_LENGTH);
		
		if($addValidationHash)
		{
			$hash = static::generateValidationHash($in);
			LowEntry::systemArrayCopy($hash, 0, $bloc, 0, static::HASH_LENGTH);
		}
		
		$engine = new EncryptionAesFastEngine1D($key);
		$engine->loadIv(static::expandIv($iv));

//		$i = 0;
		$padi = 0;
		$im16 = ($hashLength % 16);
		$inLength = \count($in);
		for($i = $hashLength; $i < ($inLength + $length + $hashLength); $i++)
		{
			if(($i > 0) && ($im16 == 0))
			{
				$engine->processBlockEncryption($bloc, 0, $tmp, (($i - 16) + static::IV_LENGTH));
			}
			if(($i - $hashLength) < $inLength)
			{
				$bloc[$im16] = $in[$i - $hashLength];
			}
			else
			{
				$bloc[$im16] = $padding[$padi];
				$padi++;
			}
			
			$im16++;
			if($im16 == 16)
			{
				$im16 = 0;
			}
		}
		$engine->processBlockEncryption($bloc, 0, $tmp, (($i - 16) + static::IV_LENGTH));
		return $tmp;
	}
	
	/**
	 * @param int[]    $in Byte array
	 * @param AesKey1D $key
	 * @param bool     $addedValidationHash
	 *
	 * @return int[] Byte array
	 */
	private static function decrypt1D($in, $key, $addedValidationHash)
	{
		if(!isset($in) || !isset($key) || ($key->rounds <= 0) || !isset($key->decryptionW))
		{
			return [];
		}
		if((\count($in) - static::IV_LENGTH) < 16)
		{
			return [];
		}
		if(((\count($in) - static::IV_LENGTH) % 16) != 0)
		{
			return [];
		}
		
		$tmp = LowEntry::createArray(\count($in) - static::IV_LENGTH, 0);
		$bloc = LowEntry::createArray(16, 0);
		
		$engine = new EncryptionAesFastEngine1D($key);
		$engine->loadIv(static::expandIv($in));

//		$i = 0;
		$im16 = 0;
		$tmpLength = \count($tmp);
		for($i = 0; $i < $tmpLength; $i++)
		{
			if(($i > 0) && ($im16 == 0))
			{
				$engine->processBlockDecryption($bloc, 0, $tmp, ($i - 16));
			}
			if($i < $tmpLength)
			{
				$bloc[$im16] = $in[$i + static::IV_LENGTH];
			}
			
			$im16++;
			if($im16 == 16)
			{
				$im16 = 0;
			}
		}
		$engine->processBlockDecryption($bloc, 0, $tmp, ($i - 16));
		
		if($addedValidationHash)
		{
			$hash = LowEntry::createArray(static::HASH_LENGTH, 0);
			LowEntry::systemArrayCopy($tmp, 0, $hash, 0, static::HASH_LENGTH);
			
			$tmp = static::deletePadding($tmp, true);
			
			$generatedHash = static::generateValidationHash($tmp);
			if(!LowEntry::areBytesEqual($hash, $generatedHash))
			{
				return [];
			}
			return $tmp;
		}
		
		return static::deletePadding($tmp, false);
	}
	
	
	/**
	 * @param int[] $key Byte array
	 *
	 * @return AesKey1D|null
	 */
	public static function createKey($key)
	{
		return EncryptionAesFastEngine1D::generateAesKey(static::fixKey($key));
	}
	
	/**
	 * @param int[] $key Byte array
	 * @param bool  $forEncryption
	 * @param bool  $forDecryption
	 *
	 * @return AesKey1D|null
	 */
	private static function createKey1DCustom($key, $forEncryption, $forDecryption)
	{
		return EncryptionAesFastEngine1D::generateAesKeyCustom(static::fixKey($key), $forEncryption, $forDecryption);
	}
	
	
	/**
	 * @param int[] $data Byte array
	 * @param int[] $key  Byte array
	 * @param bool  $addValidationHash
	 *
	 * @return int[] Byte array
	 */
	public static function encryptBytes($data, $key, $addValidationHash)
	{
		if(!isset($data))
		{
			return [];
		}
		return static::encrypt1D($data, static::createKey1DCustom($key, true, false), $addValidationHash);
	}
	
	/**
	 * @param int[] $data Byte array
	 * @param int[] $key  Byte array
	 * @param bool  $addedValidationHash
	 *
	 * @return int[] Byte array
	 */
	public static function decryptBytes($data, $key, $addedValidationHash)
	{
		if(!isset($data))
		{
			return [];
		}
		return static::decrypt1D($data, static::createKey1DCustom($key, false, true), $addedValidationHash);
	}
}
