<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Polyfill\Intl\Normalizer;

use ValueError;

use function chr;
use function defined;
use function in_array;
use function ini_get;
use function ord;
use function strlen;

use const PHP_VERSION_ID;

/**
 * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension.
 *
 * It has been validated with Unicode 6.3 Normalization Conformance Test.
 * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Normalizer {
	public const FORM_D = \Normalizer::FORM_D;
	public const FORM_KD = \Normalizer::FORM_KD;
	public const FORM_C = \Normalizer::FORM_C;
	public const FORM_KC = \Normalizer::FORM_KC;
	public const NFD = \Normalizer::NFD;
	public const NFKD = \Normalizer::NFKD;
	public const NFC = \Normalizer::NFC;
	public const NFKC = \Normalizer::NFKC;

	private static $C;
	private static $D;
	private static $KD;
	private static $cC;
	private static $ulenMask = [ "\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4 ];
	private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";

	public static function isNormalized( string $s, int $form = self::FORM_C ) {
		if ( ! in_array( $form, [ self::NFD, self::NFKD, self::NFC, self::NFKC ] ) ) {
			return false;
		}
		if ( ! isset( $s[ strspn( $s, self::$ASCII ) ] ) ) {
			return true;
		}
		if ( self::NFC == $form && preg_match( '//u', $s ) && ! preg_match( '/[^\x00-\x{2FF}]/u', $s ) ) {
			return true;
		}

		return self::normalize( $s, $form ) === $s;
	}

	public static function normalize( string $s, int $form = self::FORM_C ) {
		if ( ! preg_match( '//u', $s ) ) {
			return false;
		}

		switch ( $form ) {
			case self::NFC:
				$C = true;
				$K = false;
				break;
			case self::NFD:
				$C = false;
				$K = false;
				break;
			case self::NFKC:
				$C = true;
				$K = true;
				break;
			case self::NFKD:
				$C = false;
				$K = true;
				break;
			default:
				if ( defined( 'Normalizer::NONE' ) && \Normalizer::NONE == $form ) {
					return $s;
				}

				if ( 80000 > PHP_VERSION_ID ) {
					return false;
				}

				throw new ValueError( 'normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form' );
		}

		if ( '' === $s ) {
			return '';
		}

		if ( $K && null === self::$KD ) {
			self::$KD = self::getData( 'compatibilityDecomposition' );
		}

		if ( null === self::$D ) {
			self::$D  = self::getData( 'canonicalDecomposition' );
			self::$cC = self::getData( 'combiningClass' );
		}

		if ( null !== $mbEncoding = ( 2 /* MB_OVERLOAD_STRING */ & (int) ini_get( 'mbstring.func_overload' ) ) ? mb_internal_encoding() : null ) {
			mb_internal_encoding( '8bit' );
		}

		$r = self::decompose( $s, $K );

		if ( $C ) {
			if ( null === self::$C ) {
				self::$C = self::getData( 'canonicalComposition' );
			}

			$r = self::recompose( $r );
		}
		if ( null !== $mbEncoding ) {
			mb_internal_encoding( $mbEncoding );
		}

		return $r;
	}

	private static function recompose( $s ) {
		$ASCII     = self::$ASCII;
		$compMap   = self::$C;
		$combClass = self::$cC;
		$ulenMask  = self::$ulenMask;

		$result = $tail = '';

		$i   = $s[0] < "\x80" ? 1 : $ulenMask[ $s[0] & "\xF0" ];
		$len = strlen( $s );

		$lastUchr = substr( $s, 0, $i );
		$lastUcls = isset( $combClass[ $lastUchr ] ) ? 256 : 0;

		while ( $i < $len ) {
			if ( $s[ $i ] < "\x80" ) {
				// ASCII chars

				if ( $tail ) {
					$lastUchr .= $tail;
					$tail     = '';
				}

				if ( $j = strspn( $s, $ASCII, $i + 1 ) ) {
					$lastUchr .= substr( $s, $i, $j );
					$i        += $j;
				}

				$result   .= $lastUchr;
				$lastUchr = $s[ $i ];
				$lastUcls = 0;
				++ $i;
				continue;
			}

			$ulen = $ulenMask[ $s[ $i ] & "\xF0" ];
			$uchr = substr( $s, $i, $ulen );

			if ( $lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr
			     || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr
			     || $lastUcls ) {
				// Table lookup and combining chars composition

				$ucls = $combClass[ $uchr ] ?? 0;

				if ( isset( $compMap[ $lastUchr . $uchr ] ) && ( ! $lastUcls || $lastUcls < $ucls ) ) {
					$lastUchr = $compMap[ $lastUchr . $uchr ];
				} elseif ( $lastUcls = $ucls ) {
					$tail .= $uchr;
				} else {
					if ( $tail ) {
						$lastUchr .= $tail;
						$tail     = '';
					}

					$result   .= $lastUchr;
					$lastUchr = $uchr;
				}
			} else {
				// Hangul chars

				$L = ord( $lastUchr[2] ) - 0x80;
				$V = ord( $uchr[2] ) - 0xA1;
				$T = 0;

				$uchr = substr( $s, $i + $ulen, 3 );

				if ( "\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82" ) {
					$T = ord( $uchr[2] ) - 0xA7;
					0 > $T && $T += 0x40;
					$ulen += 3;
				}

				$L        = 0xAC00 + ( $L * 21 + $V ) * 28 + $T;
				$lastUchr = chr( 0xE0 | $L >> 12 ) . chr( 0x80 | $L >> 6 & 0x3F ) . chr( 0x80 | $L & 0x3F );
			}

			$i += $ulen;
		}

		return $result . $lastUchr . $tail;
	}

	private static function decompose( $s, $c ) {
		$result = '';

		$ASCII     = self::$ASCII;
		$decompMap = self::$D;
		$combClass = self::$cC;
		$ulenMask  = self::$ulenMask;
		if ( $c ) {
			$compatMap = self::$KD;
		}

		$c   = [];
		$i   = 0;
		$len = strlen( $s );

		while ( $i < $len ) {
			if ( $s[ $i ] < "\x80" ) {
				// ASCII chars

				if ( $c ) {
					ksort( $c );
					$result .= implode( '', $c );
					$c      = [];
				}

				$j      = 1 + strspn( $s, $ASCII, $i + 1 );
				$result .= substr( $s, $i, $j );
				$i      += $j;
				continue;
			}

			$ulen = $ulenMask[ $s[ $i ] & "\xF0" ];
			$uchr = substr( $s, $i, $ulen );
			$i    += $ulen;

			if ( $uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr ) {
				// Table lookup

				if ( $uchr !== $j = $compatMap[ $uchr ] ?? ( $decompMap[ $uchr ] ?? $uchr ) ) {
					$uchr = $j;

					$j    = strlen( $uchr );
					$ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[ $uchr[0] & "\xF0" ];

					if ( $ulen != $j ) {
						// Put trailing chars in $s

						$j -= $ulen;
						$i -= $j;

						if ( 0 > $i ) {
							$s   = str_repeat( ' ', - $i ) . $s;
							$len -= $i;
							$i   = 0;
						}

						while ( $j -- ) {
							$s[ $i + $j ] = $uchr[ $ulen + $j ];
						}

						$uchr = substr( $uchr, 0, $ulen );
					}
				}
				if ( isset( $combClass[ $uchr ] ) ) {
					// Combining chars, for sorting

					if ( ! isset( $c[ $combClass[ $uchr ] ] ) ) {
						$c[ $combClass[ $uchr ] ] = '';
					}
					$c[ $combClass[ $uchr ] ] .= $uchr;
					continue;
				}
			} else {
				// Hangul chars

				$uchr = unpack( 'C*', $uchr );
				$j    = ( ( $uchr[1] - 224 ) << 12 ) + ( ( $uchr[2] - 128 ) << 6 ) + $uchr[3] - 0xAC80;

				$uchr = "\xE1\x84" . chr( 0x80 + (int) ( $j / 588 ) )
				        . "\xE1\x85" . chr( 0xA1 + (int) ( ( $j % 588 ) / 28 ) );

				if ( $j %= 28 ) {
					$uchr .= $j < 25
						? ( "\xE1\x86" . chr( 0xA7 + $j ) )
						: ( "\xE1\x87" . chr( 0x67 + $j ) );
				}
			}
			if ( $c ) {
				ksort( $c );
				$result .= implode( '', $c );
				$c      = [];
			}

			$result .= $uchr;
		}

		if ( $c ) {
			ksort( $c );
			$result .= implode( '', $c );
		}

		return $result;
	}

	private static function getData( $file ) {
		if ( file_exists( $file = __DIR__ . '/Resources/unidata/' . $file . '.php' ) ) {
			return require $file;
		}

		return false;
	}
}
