<?php

namespace LowEntryUE4PHP\Classes\Internal;

use LowEntryUE4PHP\LowEntry;


class CompressionLzf
{
	const SKIP_LENGTH = 15;
	
	const HASH_SIZE = 65536; //(1 << 16);
	const MAX_LITERAL = 32; //(1 << 5);
	const MAX_OFF = 8192; //(1 << 13);
	const MAX_REF = 264; //((1 << 8) + (1 << 3));
	
	/** @var int[] Integer array */
	private static $hashTab;
	
	
	/**
	 * @param int $value
	 *
	 * @return int
	 */
	private static function uintByteCount($value)
	{
		return (($value <= 127) ? 1 : 4);
	}
	
	
	/**
	 * @param CompressionByteArrayBuffer $array
	 * @param int                        $arrayOffset
	 * @param int                        $value
	 */
	private static function uintToBytes($array, $arrayOffset, $value)
	{
		if($value <= 127)
		{
			$array->set($arrayOffset, LowEntry::castToByte($value));
		}
		else
		{
			$array->set($arrayOffset, LowEntry::castToByte(($value >> 24) | (1 << 7)));
			$array->set($arrayOffset + 1, LowEntry::castToByte($value >> 16));
			$array->set($arrayOffset + 2, LowEntry::castToByte($value >> 8));
			$array->set($arrayOffset + 3, LowEntry::castToByte($value));
		}
	}
	
	/**
	 * Returns -1 if invalid.
	 *
	 * @param int[] $array Byte array
	 * @param int   $arrayOffset
	 *
	 * @return int
	 */
	private static function bytesToUint($array, $arrayOffset)
	{
		if((\count($array) - 1) < $arrayOffset)
		{
			return -1;
		}
		
		
		$b = $array[$arrayOffset];
		if((($b >> 7) & 1) == 0)
		{
			return ($b & 0xFF);
		}
		if((\count($array) - 4) < $arrayOffset)
		{
			return -1;
		}
		$value = ((($b & 0xFF) & ~(1 << 7)) << 24) | (($array[$arrayOffset + 1] & 0xFF) << 16) | (($array[$arrayOffset + 2] & 0xFF) << 8) | ($array[$arrayOffset + 3] & 0xFF);
		if($value <= 127)
		{
			return -1;
		}
		return $value;
	}
	
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return int[] Byte array
	 */
	public static function compress($bytes)
	{
		$inLen = \count($bytes);
		if($inLen < static::SKIP_LENGTH)
		{
			$result = LowEntry::createArray($inLen + 1, 0);
			$result[0] = 0;
			LowEntry::systemArrayCopy($bytes, 0, $result, 1, $inLen);
			return $result;
		}
		
		
		$out = new CompressionByteArrayBuffer($inLen + 20, 512);
		$out->set(0, 1);
		static::uintToBytes($out, 1, $inLen);
		$outPos = 2 + static::uintByteCount($inLen);
		$inPos = 0;
		$literals = 0;
		$future = ($bytes[0] << 8) | ($bytes[1] & 255);
		
		//synchronized(COMPRESS_SYNCHRONIZER)
		{
			if(!isset(static::$hashTab))
			{
				static::$hashTab = LowEntry::createArray(static::HASH_SIZE, 0);
			}
			while($inPos < ($inLen - 4))
			{
				$p2 = $bytes[$inPos + 2];
				$future = ($future << 8) + ($p2 & 255);
				$off = (($future * 2777) >> 9) & (static::HASH_SIZE - 1);
				$ref = static::$hashTab[$off];
				static::$hashTab[$off] = $inPos;
				if(($ref < $inPos) && ($ref > 0) && (($off = $inPos - $ref - 1) < static::MAX_OFF) && ($bytes[$ref + 2] == $p2) && ($bytes[$ref + 1] == LowEntry::castToByte($future >> 8)) && ($bytes[$ref] == LowEntry::castToByte($future >> 16)))
				{
					$maxLen = $inLen - $inPos - 2;
					if($maxLen > static::MAX_REF)
					{
						$maxLen = static::MAX_REF;
					}
					if($literals == 0)
					{
						$outPos--;
					}
					else
					{
						$out->set($outPos - $literals - 1, LowEntry::castToByte($literals - 1));
						$literals = 0;
					}
					$len = 3;
					while(($len < $maxLen) && ($bytes[$ref + $len] == $bytes[$inPos + $len]))
					{
						$len++;
					}
					$len -= 2;
					if($len < 7)
					{
						$out->set($outPos++, LowEntry::castToByte(($off >> 8) + ($len << 5)));
					}
					else
					{
						$out->set($outPos++, LowEntry::castToByte(($off >> 8) + (7 << 5)));
						$out->set($outPos++, LowEntry::castToByte($len - 7));
					}
					$out->set($outPos++, LowEntry::castToByte($off));
					$outPos++;
					$inPos += $len;
					$future = ($bytes[$inPos] << 8) | ($bytes[$inPos + 1] & 255);
					$future = ($future << 8) | ($bytes[$inPos + 2] & 255);
					static::$hashTab[(($future * 2777) >> 9) & (static::HASH_SIZE - 1)] = $inPos++;
					$future = ($future << 8) | ($bytes[$inPos + 2] & 255);
					static::$hashTab[(($future * 2777) >> 9) & (static::HASH_SIZE - 1)] = $inPos++;
				}
				else
				{
					$out->set($outPos++, $bytes[$inPos++]);
					$literals++;
					if($literals == static::MAX_LITERAL)
					{
						$out->set($outPos - $literals - 1, LowEntry::castToByte($literals - 1));
						$literals = 0;
						$outPos++;
					}
				}
			}
		}
		
		while($inPos < $inLen)
		{
			$out->set($outPos++, $bytes[$inPos++]);
			$literals++;
			if($literals == static::MAX_LITERAL)
			{
				$out->set($outPos - $literals - 1, LowEntry::castToByte($literals - 1));
				$literals = 0;
				$outPos++;
			}
		}
		$out->set($outPos - $literals - 1, LowEntry::castToByte($literals - 1));
		if($literals == 0)
		{
			$outPos--;
		}
		
		if($outPos >= $inLen)
		{
			$result = LowEntry::createArray($inLen + 1, 0);
			$result[0] = 0;
			LowEntry::systemArrayCopy($bytes, 0, $result, 1, $inLen);
			return $result;
		}
		return $out->getData($outPos);
	}
	
	
	/**
	 * @param int[] $bytes Byte array
	 *
	 * @return int[] Byte array
	 */
	public static function decompress($bytes)
	{
		$inLen = \count($bytes);
		if($inLen < 2) // bool (1 byte), (optional: size (minimum of 1 byte)), data (minimum of 1 byte)
		{
			return [];
		}
		
		
		if($bytes[0] == 0)
		{
			$result = LowEntry::createArray($inLen - 1, 0);
			LowEntry::systemArrayCopy($bytes, 1, $result, 0, $inLen - 1);
			return $result;
		}
		if($bytes[0] != 1)
		{
			return [];
		}
		
		$outLen = static::bytesToUint($bytes, 1);
		if($outLen <= 0)
		{
			return [];
		}
		$inPos = 1 + static::uintByteCount($outLen);
		$out = new CompressionByteArrayBuffer($outLen, 512);
		$outPos = 0;
		
		do
		{
			if($inPos >= $inLen)
			{
				return [];
			}
			$ctrl = $bytes[$inPos++] & 255;
			if($ctrl < static::MAX_LITERAL)
			{
				$ctrl++;
				if(($inLen - $inPos) < $ctrl)
				{
					return [];
				}
				$out->set($outPos, $bytes, $inPos, $ctrl);
				$outPos += $ctrl;
				$inPos += $ctrl;
			}
			else
			{
				$len = $ctrl >> 5;
				if($len == 7)
				{
					if($inPos >= $inLen)
					{
						return [];
					}
					$len += $bytes[$inPos++] & 255;
				}
				$len += 2;
				$ctrl = -(($ctrl & 0x1f) << 8) - 1;
				if($inPos >= $inLen)
				{
					return [];
				}
				$ctrl -= $bytes[$inPos++] & 255;
				$ctrl += $outPos;
				for($i = 0; $i < $len; $i++)
				{
					if(($outPos < 0) || ($outPos >= $outLen))
					{
						return [];
					}
					if(($ctrl < 0) || ($ctrl >= $outLen))
					{
						return [];
					}
					$out->set($outPos++, $out->get($ctrl++));
				}
			}
		}
		while($outPos < $outLen);
		return $out->getData();
	}
}
