<?php

namespace LowEntryUE4PHP;

use LowEntryUE4PHP\Classes\Internal\CompressionLzf;
use LowEntryUE4PHP\Classes\Internal\EncryptionAes;
use LowEntryUE4PHP\Classes\Internal\EncryptionRsa;
use LowEntryUE4PHP\Classes\RsaKeys;
use LowEntryUE4PHP\Classes\RsaPrivateKey;
use LowEntryUE4PHP\Classes\RsaPublicKey;


class LowEntry
{
	/**
	 * @param int   $length
	 * @param mixed $defaultValue
	 *
	 * @return mixed[]
	 */
	public static function createArray($length, $defaultValue)
	{
		if($length > 0)
		{
			return \array_fill(0, $length, $defaultValue);
		}
		return [];
	}
	
	/**
	 * @param mixed[] $array
	 * @param int     $newLength
	 * @param mixed   $defaultValue
	 *
	 * @return mixed[]
	 */
	public static function arraysCopyOf($array, $newLength, $defaultValue)
	{
		if(\count($array) > $newLength)
		{
			return \array_slice($array, 0, $newLength);
		}
		if(\count($array) == $newLength)
		{
			return $array;
		}
		$newArray = static::createArray($newLength, $defaultValue);
		static::systemArrayCopy($array, 0, $newArray, 0, \count($array));
		return $newArray;
	}
	
	/**
	 * @param mixed[] $array
	 * @param int     $from
	 * @param int     $to
	 * @param mixed   $defaultValue
	 *
	 * @return mixed[]
	 */
	public static function arraysCopyOfRange($array, $from, $to, $defaultValue)
	{
		$newLength = $to - $from;
		if($newLength < 0)
		{
			return [];
		}
		$arrayLength = \count($array);
		if($from >= $arrayLength)
		{
			return [];
		}
		if(($from == 0) && ($to == $arrayLength))
		{
			return $array;
		}
		$newArray = static::createArray($newLength, $defaultValue);
		if($newLength > $arrayLength - $from)
		{
			$newLength = $arrayLength - $from;
		}
		static::systemArrayCopy($array, $from, $newArray, 0, $newLength);
		return $newArray;
	}
	
	/**
	 * @param mixed[] $src
	 * @param int     $srcPos
	 * @param mixed[] $dest
	 * @param int     $destPos
	 * @param int     $length
	 */
	public static function systemArrayCopy($src, $srcPos, &$dest, $destPos, $length)
	{
		for($i = 0; $i < $length; $i++)
		{
			$dest[$destPos + $i] = $src[$srcPos + $i];
		}
	}
	
	/**
	 * @param int $integer
	 *
	 * @return int
	 */
	public static function castToByte($integer)
	{
		return ($integer & 255);
	}
	
	/**
	 * @param int $a
	 * @param int $b
	 *
	 * @return int
	 */
	public static function tripleShiftRight($a, $b)
	{
		if($b >= 32 || $b < -32)
		{
			$m = (int) ($b / 32);
			$b = $b - ($m * 32);
		}
		
		if($b < 0)
		{
			$b = (int) (32 + $b);
		}
		
		if($b == 0)
		{
			return ((($a >> 1) & 0x7fffffff) * 2 + (($a >> $b) & 1));
		}
		
		if($a < 0)
		{
			$a = ($a >> 1);
			$a &= 0x7fffffff;
			$a |= 0x40000000;
			$a = ($a >> ($b - 1));
		}
		else
		{
			$a = ($a >> $b);
		}
		return (int) $a;
	}
	
	/**
	 * @param int $a
	 * @param int $b
	 *
	 * @return int
	 */
	public static function tripleShiftRightLong($a, $b)
	{
		if($a >= 0)
		{
			return ($a >> $b);
		}
		else
		{
			return ((($a & 0x7fffffffffffffff) >> $b) | (0x4000000000000000 >> ($b - 1)));
		}
	}
	
	
	/**
	 * @param int $length
	 *
	 * @return int[] Byte array
	 */
	public static function randomBytes($length)
	{
		$bytes = LowEntry::createArray($length, 0);
		for($i = 0; $i < $length; $i++)
		{
			$bytes[$i] = LowEntry::castToByte(\mt_rand());
		}
		return $bytes;
	}
	
	/**
	 * @param int[] $a Byte array
	 * @param int[] $b Byte array
	 *
	 * @return bool
	 */
	public static function areBytesEqual($a, $b)
	{
		if(!isset($a) || !isset($b))
		{
			return false;
		}
		if(\count($a) != \count($b))
		{
			return false;
		}
		
		$aLength = \count($a);
		for($i = 0; $i < $aLength; $i++)
		{
			if($a[$i] != $b[$i])
			{
				return false;
			}
		}
		return true;
	}
	
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return string
	 */
	public static function bytesToStringUtf8($bytes)
	{
		\array_unshift($bytes, 'C*');
		return \pack(...$bytes);
	}
	
	/**
	 * @param string $string
	 *
	 * @return int[] Byte array
	 */
	public static function stringToBytesUtf8($string)
	{
		return \array_values(\unpack('C*', $string));
	}
	
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return string
	 */
	public static function bytesToHex($bytes)
	{
		$chars = \array_map('chr', $bytes);
		$bin = \implode($chars);
		return \strtoupper(\bin2hex($bin));
	}
	
	/**
	 * @param string $string
	 *
	 * @return int[] Byte array
	 */
	public static function hexToBytes($string)
	{
		return static::stringToBytesUtf8(\pack('H*', $string));
	}
	
	
	/**
	 * @param int[] $bytes Byte array
	 * @param bool  $addSpaces
	 *
	 * @return string
	 */
	public static function bytesToBinary($bytes, $addSpaces = false)
	{
		if(empty($bytes))
		{
			return '';
		}
		$bytesCount = \count($bytes);
		$stringBuilder = '';
		for($i = 1; $i <= $bytesCount; $i++)
		{
			for($j = 7; $j >= 0; $j--)
			{
				if(($bytes[$i - 1] & (1 << $j)) > 0)
				{
					$stringBuilder .= '1';
				}
				else
				{
					$stringBuilder .= '0';
				}
			}
			if($addSpaces && ($i < $bytesCount))
			{
				$stringBuilder .= ' ';
			}
		}
		return $stringBuilder;
	}
	
	/**
	 * @param string $binary
	 *
	 * @return int[] Byte array
	 */
	public static function binaryToBytes($binary)
	{
		if(empty($binary))
		{
			return [];
		}
		$binary = \str_replace(' ', '', $binary);
		$binaryCount = \count($binary);
		if(($binaryCount <= 0) || (($binaryCount % 8) != 0))
		{
			return [];
		}
		$bits = \str_split($binary);
		$bytes = self::createArray($binaryCount / 8, 0);
		$index = 0;
		for($i = 0; $i < $binaryCount; $i += 8)
		{
			$b = 0;
			for($j = 0; $j < 8; $j++)
			{
				if($bits[$i + $j] == '0')
				{
					continue;
				}
				elseif($bits[$i + $j] == '1')
				{
					$b += (1 << (7 - $j));
				}
				else
				{
					// encountered invalid character
					return [];
				}
			}
			$bytes[$index] = $b;
			$index++;
		}
		return $bytes;
	}
	
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return string
	 */
	public static function bytesToBase64($bytes)
	{
		$chars = \array_map('chr', $bytes);
		$bin = \implode($chars);
		return \base64_encode($bin);
	}
	
	/**
	 * @param string $string
	 *
	 * @return int[] Byte array
	 */
	public static function base64ToBytes($string)
	{
		return static::stringToBytesUtf8(\base64_decode($string));
	}
	
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return bool
	 */
	public static function bytesToBoolean($bytes)
	{
		if(empty($bytes))
		{
			return false;
		}
		return ($bytes[0] == 1);
	}
	
	/**
	 * @param bool $value
	 *
	 * @return int[] Byte array
	 */
	public static function booleanToBytes($value)
	{
		if($value)
		{
			return [1];
		}
		else
		{
			return [0];
		}
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return bool
	 */
	public static function byteToBoolean($bytes)
	{
		return ($bytes == 1);
	}
	
	/**
	 * @param bool $value
	 *
	 * @return int Byte
	 */
	public static function booleanToByte($value)
	{
		if($value)
		{
			return 1;
		}
		else
		{
			return 0;
		}
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return int
	 */
	public static function bytesToInteger($bytes)
	{
		if(empty($bytes))
		{
			return 0;
		}
		
		$bytesCount = \count($bytes);
		if($bytesCount <= 1)
		{
			return ($bytes[0] & 0xFF);
		}
		if($bytesCount <= 2)
		{
			return (($bytes[0] & 0xFF) << 8) | ($bytes[1] & 0xFF);
		}
		if($bytesCount <= 3)
		{
			return (($bytes[0] & 0xFF) << 16) | (($bytes[1] & 0xFF) << 8) | ($bytes[2] & 0xFF);
		}
		return (($bytes[0] & 0xFF) << 24) | (($bytes[1] & 0xFF) << 16) | (($bytes[2] & 0xFF) << 8) | ($bytes[3] & 0xFF);
	}
	
	/**
	 * @param int $value
	 *
	 * @return int[] Byte array
	 */
	public static function integerToBytes($value)
	{
		return [LowEntry::castToByte($value >> 24), LowEntry::castToByte($value >> 16), LowEntry::castToByte($value >> 8), LowEntry::castToByte($value)];
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return float
	 */
	public static function bytesToFloat($bytes)
	{
		$array = \unpack('f', \pack('l', self::bytesToInteger($bytes)));
		return \end($array);
	}
	
	/**
	 * @param float $value
	 *
	 * @return int[] Byte array
	 */
	public static function floatToBytes($value)
	{
		$array = \unpack('l', \pack('f', $value));
		return self::integerToBytes(\end($array));
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return int
	 */
	public static function bytesToLong($bytes)
	{
		if(empty($bytes))
		{
			return 0;
		}
		$bytesCount = \count($bytes);
		if($bytesCount <= 1)
		{
			return ($bytes[0] & 0xFF);
		}
		if($bytesCount <= 2)
		{
			return (($bytes[0] & 0xFF) << 8) | ($bytes[1] & 0xFF);
		}
		if($bytesCount <= 3)
		{
			return (($bytes[0] & 0xFF) << 16) | (($bytes[1] & 0xFF) << 8) | ($bytes[2] & 0xFF);
		}
		if($bytesCount <= 4)
		{
			return (($bytes[0] & 0xFF) << 24) | (($bytes[1] & 0xFF) << 16) | (($bytes[2] & 0xFF) << 8) | ($bytes[3] & 0xFF);
		}
		if($bytesCount <= 5)
		{
			return (($bytes[0] & 0xFF) << 32) | (($bytes[1] & 0xFF) << 24) | (($bytes[2] & 0xFF) << 16) | (($bytes[3] & 0xFF) << 8) | ($bytes[4] & 0xFF);
		}
		if($bytesCount <= 6)
		{
			return (($bytes[0] & 0xFF) << 40) | (($bytes[1] & 0xFF) << 32) | (($bytes[2] & 0xFF) << 24) | (($bytes[3] & 0xFF) << 16) | (($bytes[4] & 0xFF) << 8) | ($bytes[5] & 0xFF);
		}
		if($bytesCount <= 7)
		{
			return (($bytes[0] & 0xFF) << 48) | (($bytes[1] & 0xFF) << 40) | (($bytes[2] & 0xFF) << 32) | (($bytes[3] & 0xFF) << 24) | (($bytes[4] & 0xFF) << 16) | (($bytes[5] & 0xFF) << 8) | ($bytes[6] & 0xFF);
		}
		return (($bytes[0] & 0xFF) << 56) | (($bytes[1] & 0xFF) << 48) | (($bytes[2] & 0xFF) << 40) | (($bytes[3] & 0xFF) << 32) | (($bytes[4] & 0xFF) << 24) | (($bytes[5] & 0xFF) << 16) | (($bytes[6] & 0xFF) << 8) | ($bytes[7] & 0xFF);
	}
	
	/**
	 * @param int $value
	 *
	 * @return int[] Byte array
	 */
	public static function longToBytes($value)
	{
		return [LowEntry::castToByte($value >> 56), LowEntry::castToByte($value >> 48), LowEntry::castToByte($value >> 40), LowEntry::castToByte($value >> 32), LowEntry::castToByte($value >> 24), LowEntry::castToByte($value >> 16), LowEntry::castToByte($value >> 8), LowEntry::castToByte($value)];
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return float
	 */
	public static function bytesToDouble($bytes)
	{
		$array = \unpack('d', \pack('q', self::bytesToLong($bytes)));
		return \end($array);
	}
	
	/**
	 * @param float $value
	 *
	 * @return int[] Byte array
	 */
	public static function doubleToBytes($value)
	{
		$array = \unpack('q', \pack('d', $value));
		return self::longToBytes(\end($array));
	}
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return int Byte
	 */
	public static function bytesToByte($bytes)
	{
		if(empty($bytes))
		{
			return 0;
		}
		return LowEntry::castToByte($bytes[0]);
	}
	
	/**
	 * @param int $value Byte
	 *
	 * @return int[] Byte array
	 */
	public static function byteToBytes($value)
	{
		return [LowEntry::castToByte($value)];
	}
	
	
	/**
	 * Prints a message using print_r, wrapped in pre tags.
	 *
	 * @param mixed $value
	 */
	public static function debug($value)
	{
		if(\is_array($value) || \is_object($value) || \is_string($value))
		{
			echo '<pre>' . print_r($value, true) . '</pre>';
		}
		else
		{
			echo '<pre>' . var_export($value, true) . '</pre>';
		}
	}
	
	
	/**
	 * @param string $algorithm
	 * @param int[]  $bytes Byte array
	 *
	 * @return int[] Byte array
	 */
	public static function hash($algorithm, $bytes)
	{
		return self::stringToBytesUtf8(\hash($algorithm, self::bytesToStringUtf8($bytes), true));
	}
	
	
	/**
	 * Compresses the given bytes with LZF.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[] $data Byte array
	 *
	 * @return int[] Byte array
	 */
	public static function compressLzf($data)
	{
		if(!isset($data) || !\is_array($data))
		{
			return [];
		}
		return CompressionLzf::compress($data);
	}
	
	/**
	 * Tries to decompress the given bytes with LZF, returns an empty array if it fails.<br>
	 * This method will fail if the given bytes are not compressed with LZF.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[] $data Byte array
	 *
	 * @return int[] Byte array
	 */
	public static function decompressLzf($data)
	{
		if(!isset($data) || !\is_array($data))
		{
			return [];
		}
		return CompressionLzf::decompress($data);
	}
	
	
	/**
	 * Encrypts the given bytes with AES.<br>
	 * <br>
	 * AES-128 will be used if the given key is &lt;= 16 bytes (the given key will be converted to 16 bytes then).<br>
	 * AES-192 will be used if the given key is &gt; 16 bytes and &lt;= 24 bytes (the given key will be converted to 24 bytes then).<br>
	 * AES-256 will be used if the given key is &gt; 25 bytes (the given key will be converted to 32 bytes then).<br>
	 * <br>
	 * You can choose to add a hash to the encrypted data. With it, the decryption function can determine if the data was (probably) not modified.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[] $data Byte array
	 * @param int[] $key  Byte array
	 * @param bool  $addValidationHash
	 *
	 * @return int[] Byte array
	 */
	public static function encryptAes($data, $key, $addValidationHash = false)
	{
		return EncryptionAes::encryptBytes($data, $key, $addValidationHash);
	}
	
	/**
	 * Decrypts the given bytes with AES.<br>
	 * <br>
	 * If (during encryption) you've added a hash to the data, you'll have to give pass true for 'addedValidationHash'.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[] $data Byte array
	 * @param int[] $key  Byte array
	 * @param bool  $addedValidationHash
	 *
	 * @return int[] Byte array
	 */
	public static function decryptAes($data, $key, $addedValidationHash = false)
	{
		return EncryptionAes::decryptBytes($data, $key, $addedValidationHash);
	}
	
	
	/**
	 * Generates a public and private key for RSA.<br>
	 * <br>
	 * The amount if bits determine the maximum amount of data you can encrypt with the generated keys.<br>
	 * The maximum amount of bytes you can encrypt is: (bits / 8) - 34.<br>
	 * <br>
	 * 512 bits = max 30 bytes of data<br>
	 * 1024 bits = max 94 bytes of data<br>
	 * 2048 bits = max 222 bytes of data<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int $bits
	 *
	 * @return RsaKeys|null
	 */
	public static function generateRsaKeys($bits)
	{
		return EncryptionRsa::generateKeys($bits);
	}
	
	/**
	 * Encrypts the given bytes with the RSA public key.<br>
	 * <br>
	 * The maximum amount of bytes the given data can have is: (rsa keys bits / 8) - 34.<br>
	 * If the given data is longer, it will be cut off to the maximum amount of bytes.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[]        $data Byte array
	 * @param RsaPublicKey $publicKey
	 *
	 * @return int[] Byte array
	 */
	public static function encryptRsa($data, $publicKey)
	{
		return EncryptionRsa::encrypt($data, $publicKey);
	}
	
	/**
	 * Decrypts the given bytes with the RSA private key.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[]         $data Byte array
	 * @param RsaPrivateKey $privateKey
	 *
	 * @return int[] Byte array
	 */
	public static function decryptRsa($data, $privateKey)
	{
		return EncryptionRsa::decrypt($data, $privateKey);
	}
	
	/**
	 * Returns a signature of the given hash with the RSA private key.<br>
	 * <br>
	 * The maximum amount of bytes the given hash can have is: (rsa keys bits / 16) - 3.<br>
	 * If the given hash is longer, this function will fail, causing it to return an empty byte array.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[]         $hash Byte array
	 * @param RsaPrivateKey $privateKey
	 *
	 * @return int[] Byte array
	 */
	public static function signRsa($hash, $privateKey)
	{
		return EncryptionRsa::sign($hash, $privateKey);
	}
	
	/**
	 * Verifies that the given signature can be decoded with the RSA public key, and that it has the expected hash in it.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[]        $signature    Byte array
	 * @param int[]        $expectedHash Byte array
	 * @param RsaPublicKey $publicKey
	 *
	 * @return bool
	 */
	public static function verifySignatureRsa($signature, $expectedHash, $publicKey)
	{
		return EncryptionRsa::verifySignature($signature, $expectedHash, $publicKey);
	}
	
	/**
	 * Converts the given RSA public key to (variable amount) of bytes.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param RsaPublicKey $publicKey
	 *
	 * @return int[] Byte array
	 */
	public static function rsaPublicKeyToBytes($publicKey)
	{
		return EncryptionRsa::publicKeyToBytes($publicKey);
	}
	
	/**
	 * Converts the given bytes to a RSA public key.<br>
	 * <br>
	 * Can return null if it fails.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[] $bytes Byte array
	 *
	 * @return RsaPublicKey|null
	 */
	public static function bytesToRsaPublicKey($bytes)
	{
		return EncryptionRsa::bytesToPublicKey($bytes);
	}
	
	/**
	 * Converts the given RSA private key to (variable amount) of bytes.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param RsaPrivateKey $privateKey
	 *
	 * @return int[] Byte array
	 */
	public static function rsaPrivateKeyToBytes($privateKey)
	{
		return EncryptionRsa::privateKeyToBytes($privateKey);
	}
	
	/**
	 * Converts the given bytes to a RSA public key.<br>
	 * <br>
	 * Can return null if it fails.<br>
	 * <br>
	 * Mimics the UE4 blueprint.
	 *
	 * @param int[] $bytes Byte array
	 *
	 * @return RsaPrivateKey|null
	 */
	public static function bytesToRsaPrivateKey($bytes)
	{
		return EncryptionRsa::bytesToPrivateKey($bytes);
	}
}
