/*
   +----------------------------------------------------------------------+
   | Copyright © The PHP Group and Contributors.                          |
   +----------------------------------------------------------------------+
   | This source file is subject to the Modified BSD License that is      |
   | bundled with this package in the file LICENSE, and is available      |
   | through the World Wide Web at <https://www.php.net/license/>.        |
   |                                                                      |
   | SPDX-License-Identifier: BSD-3-Clause                                |
   +----------------------------------------------------------------------+
   | Authors: Kirti Velankar <kirtig@yahoo-inc.com>                       |
   +----------------------------------------------------------------------+
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <unicode/ustring.h>
#include <unicode/udata.h>
#include <unicode/putil.h>
#include <unicode/ures.h>

extern "C" {
#include "php_intl.h"
#include "locale.h"
#include "locale_class.h"
#include "intl_convert.h"
#include "intl_data.h"

#include <zend_API.h>
#include <zend.h>
#include <php.h>
#include "main/php_ini.h"
#include "zend_smart_str.h"
}

ZEND_EXTERN_MODULE_GLOBALS( intl )

/* Sizes required for the strings "variant15" , "extlang11", "private12" etc. */
#define SEPARATOR "_"
#define SEPARATOR1 "-"
#define DELIMITER "-_"
#define EXTLANG_PREFIX "a"
#define PRIVATE_PREFIX "x"
#define DISP_NAME "name"
#define DISP_KEYWORD "keyword"
#define DISP_KEYWORD_VALUE "keyword_value"

#define MAX_NO_VARIANT  15
#define MAX_NO_EXTLANG  3
#define MAX_NO_PRIVATE  15
#define MAX_NO_LOOKUP_LANG_TAG  100

#define LOC_NOT_FOUND 1

/* Sizes required for the strings "variant15" , "extlang3", "private12" etc. */
#define VARIANT_KEYNAME_LEN  11
#define EXTLANG_KEYNAME_LEN  10
#define PRIVATE_KEYNAME_LEN  11

/* Based on the IANA language subtag registry (File-Date: 2021-08-06)
 * https://www.iana.org/assignments/language-subtag-registry
 *
 * This list includes all grandfathered tags, as well as redundant
 * tags that have a Preferred-Value.
 */
static const char * const LOC_GRANDFATHERED[] = {
	"art-lojban",
	"cel-gaulish",
	"en-GB-oed",
	"i-ami",
	"i-bnn",
	"i-default",
	"i-enochian",
	"i-hak",
	"i-klingon",
	"i-lux",
	"i-mingo",
	"i-navajo",
	"i-pwn",
	"i-tao",
	"i-tay",
	"i-tsu",
	"no-bok",
	"no-nyn",
	"sgn-BE-FR",
	"sgn-BE-NL",
	"sgn-BR",
	"sgn-CH-DE",
	"sgn-CO",
	"sgn-DE",
	"sgn-DK",
	"sgn-ES",
	"sgn-FR",
	"sgn-GB",
	"sgn-GR",
	"sgn-IE",
	"sgn-IT",
	"sgn-JP",
	"sgn-MX",
	"sgn-NI",
	"sgn-NL",
	"sgn-NO",
	"sgn-PT",
	"sgn-SE",
	"sgn-US",
	"sgn-ZA",
	"zh-cmn",
	"zh-cmn-Hans",
	"zh-cmn-Hant",
	"zh-gan",
	"zh-guoyu",
	"zh-hakka",
	"zh-min",
	"zh-min-nan",
	"zh-wuu",
	"zh-xiang",
	NULL
};

/* Based on the IANA language subtag registry (File-Date: 2021-08-06)
 *
 * This array lists the preferred values for the grandfathered and redundant
 * tags listed in LOC_GRANDFATHERED. This is in sync with the array
 * LOC_GRANDFATHERED, i.e., the offsets of the grandfathered tags match the
 * offsets of the preferred value. If a value in LOC_PREFERRED_GRANDFATHERED is
 * NULL, then the matching offset in LOC_GRANDFATHERED has no preferred value.
 */
static const char * const LOC_PREFERRED_GRANDFATHERED[] = {
	"jbo",
	NULL,
	"en-GB-oxendict",
	"ami",
	"bnn",
	NULL,
	NULL,
	"hak",
	"tlh",
	"lb",
	NULL,
	"nv",
	"pwn",
	"tao",
	"tay",
	"tsu",
	"nb",
	"nn",
	"sfb",
	"vgt",
	"bzs",
	"sgg",
	"csn",
	"gsg",
	"dsl",
	"ssp",
	"fsl",
	"bfi",
	"gss",
	"isg",
	"ise",
	"jsl",
	"mfs",
	"ncs",
	"dse",
	"nsl",
	"psr",
	"swl",
	"ase",
	"sfs",
	"cmn",
	"cmn-Hans",
	"cmn-Hant",
	"gan",
	"cmn",
	"hak",
	NULL,
	"nan",
	"wuu",
	"hsn",
	NULL
};

/* returns true if a is an ID separator, false otherwise */
#define isIDSeparator(a) (a == '_' || a == '-')
#define isKeywordSeparator(a) (a == '@' )
#define isEndOfTag(a) (a == '\0' )

#define isPrefixLetter(a) ((a=='x')||(a=='X')||(a=='i')||(a=='I'))

/*returns true if one of the special prefixes is here (s=string)
  'x-' or 'i-' */
#define isIDPrefix(s) (isPrefixLetter(s[0])&&isIDSeparator(s[1]))
#define isKeywordPrefix(s) ( isKeywordSeparator(s[0]) )

/* Dot terminates it because of POSIX form  where dot precedes the codepage
 * except for variant */
#define isTerminator(a)  ((a==0)||(a=='.')||(a=='@'))

/* {{{ return the offset of 'key' in the array 'list'.
 * returns -1 if not present */
static int16_t findOffset(const char* const* list, const char* key)
{
	const char* const* anchor = list;
	while (*list != NULL) {
		if (strcasecmp(key, *list) == 0) {
			return (int16_t)(list - anchor);
		}
		list++;
	}

	return -1;

}
/*}}}*/

static char* getPreferredTag(const char* gf_tag)
{
	char* result = NULL;
	zend_off_t grOffset = 0;

	grOffset = findOffset( LOC_GRANDFATHERED ,gf_tag);
	if(grOffset < 0) {
		return NULL;
	}
	if( LOC_PREFERRED_GRANDFATHERED[grOffset] != NULL ){
		/* return preferred tag */
		result = estrdup( LOC_PREFERRED_GRANDFATHERED[grOffset] );
	} else {
		/* Return correct grandfathered language tag */
		result = estrdup( LOC_GRANDFATHERED[grOffset] );
	}
	return result;
}

/* {{{
* returns the position of next token for lookup
* or -1 if no token
* strtokr equivalent search for token in reverse direction
*/
static zend_off_t getStrrtokenPos(char* str, zend_off_t savedPos)
{
	zend_off_t result =-1;
	zend_off_t i;

	for(i=savedPos-1; i>=0; i--) {
		if(isIDSeparator(*(str+i)) || isKeywordSeparator(*(str+i))){
			/* delimiter found; check for singleton */
			if(i>=2 && isIDSeparator(*(str+i-2)) ){
				/* a singleton; so send the position of token before the singleton */
				result = i-2;
			} else {
				result = i;
			}
			break;
		}
	}
	if(result < 1){
		/* Just in case invalid locale e.g. '-x-xyz' or '-sl_Latn' */
		result =-1;
	}
	return result;
}
/* }}} */

/* {{{
* returns the position of a singleton if present
* returns -1 if no singleton
* strtok equivalent search for singleton
*/
static zend_off_t getSingletonPos(const char* str)
{
	zend_off_t result =-1;
	size_t len = 0;

	if( str && ((len=strlen(str))>0) ){
		zend_off_t i = 0;
		for( i=0; (size_t)i < len ; i++){
			if( isIDSeparator(*(str+i)) ){
				if( i==1){
					/* string is of the form x-avy or a-prv1 */
					result =0;
					break;
				} else {
					/* delimiter found; check for singleton */
					if( isIDSeparator(*(str+i+2)) ){
						/* a singleton; so send the position of separator before singleton */
						result = i+1;
						break;
					}
				}
			}
		}/* end of for */

	}
	return result;
}
/* }}} */

/* {{{ Get default locale */
/* }}} */
/* {{{ Get default locale */
U_CFUNC PHP_NAMED_FUNCTION(zif_locale_get_default)
{
	ZEND_PARSE_PARAMETERS_NONE();

	RETURN_STRING( intl_locale_get_default(  ) );
}

/* }}} */

/* {{{ Set default locale */
/* }}} */
/* {{{ Set default locale */
U_CFUNC PHP_NAMED_FUNCTION(zif_locale_set_default)
{
	zend_string* locale_name;
	zend_string *ini_name;
	char *default_locale = NULL;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH_STR(locale_name)
	ZEND_PARSE_PARAMETERS_END();

	if (ZSTR_LEN(locale_name) == 0) {
		default_locale = (char *)uloc_getDefault();
		locale_name = zend_string_init(default_locale, strlen(default_locale), 0);
	}

	ini_name = zend_string_init(LOCALE_INI_NAME, sizeof(LOCALE_INI_NAME) - 1, 0);
	zend_alter_ini_entry(ini_name, locale_name, PHP_INI_USER, PHP_INI_STAGE_RUNTIME);
	zend_string_release_ex(ini_name, 0);
	if (default_locale != NULL) {
		zend_string_release_ex(locale_name, 0);
	}

	RETURN_TRUE;
}
/* }}} */

/* {{{
* Gets the value from ICU
* common code shared by get_primary_language,get_script or get_region or get_variant
* result = 0 if error, 1 if successful , -1 if no value
*/
static zend_string* get_icu_value_internal( const char* loc_name , const char* tag_name, int* result , int fromParseLocale)
{
	zend_string* tag_value	    = NULL;
	int32_t      tag_value_len  = 512;

	char*        mod_loc_name   = NULL;

	int32_t      buflen         = 512;
	UErrorCode   status         = U_ZERO_ERROR;

	if (UNEXPECTED(strlen(loc_name) > INTL_MAX_LOCALE_LEN)) {
		return NULL;
	}

	if( strcmp(tag_name, LOC_CANONICALIZE_TAG) != 0 ){
		/* Handle  grandfathered languages */
		zend_off_t grOffset =  findOffset( LOC_GRANDFATHERED , loc_name );
		if( grOffset >= 0 ){
			if( strcmp(tag_name , LOC_LANG_TAG)==0 ){
				return zend_string_init(loc_name, strlen(loc_name), 0);
			} else {
				/* Since Grandfathered , no value , do nothing , return NULL */
				return NULL;
			}
		}

	if( fromParseLocale==1 ){
		zend_off_t singletonPos = 0;

		/* Handle singletons */
		if( strcmp(tag_name , LOC_LANG_TAG)==0 ){
			if( strlen(loc_name)>1 && (isIDPrefix(loc_name) == 1) ){
				return zend_string_init(loc_name, strlen(loc_name), 0);
			}
		}

		singletonPos = getSingletonPos( loc_name );
		if( singletonPos == 0){
			/* singleton at start of script, region , variant etc.
			 * or invalid singleton at start of language */
			return NULL;
		} else if( singletonPos > 0 ){
			/* singleton at some position except at start
			 * strip off the singleton and rest of the loc_name */
			mod_loc_name = estrndup ( loc_name , singletonPos-1);
		}
	} /* end of if fromParse */

	} /* end of if != LOC_CANONICAL_TAG */

	if( mod_loc_name == NULL){
		mod_loc_name = estrdup(loc_name );
	}

	/* Proceed to ICU */
	do{
		if (tag_value) {
			tag_value = zend_string_realloc( tag_value , buflen, 0);
		} else {
			tag_value = zend_string_alloc( buflen, 0);
		}
		tag_value_len = buflen;

		if( strcmp(tag_name , LOC_SCRIPT_TAG)==0 ){
			buflen = uloc_getScript ( mod_loc_name , tag_value->val , tag_value_len , &status);
		}
		if( strcmp(tag_name , LOC_LANG_TAG )==0 ){
			buflen = uloc_getLanguage ( mod_loc_name , tag_value->val , tag_value_len , &status);
		}
		if( strcmp(tag_name , LOC_REGION_TAG)==0 ){
			buflen = uloc_getCountry ( mod_loc_name , tag_value->val , tag_value_len , &status);
		}
		if( strcmp(tag_name , LOC_VARIANT_TAG)==0 ){
			buflen = uloc_getVariant ( mod_loc_name , tag_value->val , tag_value_len , &status);
		}
		if( strcmp(tag_name , LOC_CANONICALIZE_TAG)==0 ){
			buflen = uloc_canonicalize ( mod_loc_name , tag_value->val , tag_value_len , &status);
		}

		if( U_FAILURE( status ) ) {
			if( status == U_BUFFER_OVERFLOW_ERROR ) {
				status = U_ZERO_ERROR;
				buflen++; /* add space for \0 */
				continue;
			}

			/* Error in retrieving data */
			*result = 0;
			if( tag_value ){
				zend_string_release_ex( tag_value, 0 );
			}
			if( mod_loc_name ){
				efree( mod_loc_name);
			}
			return NULL;
		}
	} while( buflen > tag_value_len );

	if(  buflen ==0 ){
		/* No value found */
		*result = -1;
		if( tag_value ){
			zend_string_release_ex( tag_value, 0 );
		}
		if( mod_loc_name ){
			efree( mod_loc_name);
		}
		return NULL;
	} else {
		*result = 1;
	}

	if( mod_loc_name ){
		efree( mod_loc_name);
	}

	tag_value->len = buflen;
	return tag_value;
}
/* }}} */

/* {{{
* Gets the value from ICU , called when PHP userspace function is called
* common code shared by get_primary_language,get_script or get_region or get_variant
*/
static void get_icu_value_src_php( const char* tag_name, INTERNAL_FUNCTION_PARAMETERS)
{

	char*          loc_name        	= NULL;
	size_t         loc_name_len    	= 0;

	zend_string*   tag_value		= NULL;

	int         result    		= 0;
	char*       msg        		= NULL;

	UErrorCode  status          	= U_ZERO_ERROR;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH(loc_name, loc_name_len)
	ZEND_PARSE_PARAMETERS_END();

	if(loc_name_len == 0) {
		loc_name = (char *)intl_locale_get_default();
		loc_name_len = strlen(loc_name);
	}

	INTL_CHECK_LOCALE_LEN(loc_name_len);

	/* Call ICU get */
	tag_value = get_icu_value_internal( loc_name , tag_name , &result ,0);

	/* No value found */
	if( result == -1 ) {
		if( tag_value){
			zend_string_release_ex( tag_value, 0 );
		}
		RETURN_EMPTY_STRING();
	}

	/* value found */
	if( tag_value){
		RETURN_STR( tag_value );
	}

	/* Error encountered while fetching the value */
	if( result ==0) {
		spprintf(&msg , 0, "unable to get locale %s", tag_name );
		intl_error_set( NULL, status, msg);
		efree(msg);
		RETURN_NULL();
	}

}
/* }}} */

/* {{{ gets the script for the $locale */
U_CFUNC PHP_FUNCTION( locale_get_script )
{
	get_icu_value_src_php( LOC_SCRIPT_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{ gets the region for the $locale */
U_CFUNC PHP_FUNCTION( locale_get_region )
{
	get_icu_value_src_php( LOC_REGION_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{ gets the primary language for the $locale */
U_CFUNC PHP_FUNCTION(locale_get_primary_language )
{
	get_icu_value_src_php( LOC_LANG_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */


/* {{{
 * common code shared by display_xyz functions to  get the value from ICU
 }}} */
static void get_icu_disp_value_src_php( const char* tag_name, INTERNAL_FUNCTION_PARAMETERS)
{
	char*          loc_name        	= NULL;
	size_t         loc_name_len    	= 0;

	char*       disp_loc_name       = NULL;
	size_t      disp_loc_name_len   = 0;
	int         free_loc_name       = 0;

	UChar*      disp_name      	= NULL;
	int32_t     disp_name_len  	= 0;

	char*       mod_loc_name        = NULL;

	int32_t     buflen          	= 512;
	UErrorCode  status          	= U_ZERO_ERROR;

	zend_string* u8str;

	char*       msg             	= NULL;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(1, 2)
		Z_PARAM_PATH(loc_name, loc_name_len)
		Z_PARAM_OPTIONAL
		Z_PARAM_PATH_OR_NULL(disp_loc_name, disp_loc_name_len)
	ZEND_PARSE_PARAMETERS_END();

	if (UNEXPECTED(loc_name_len > ULOC_FULLNAME_CAPACITY)) {
		/* See bug 67397: overlong locale names cause trouble in uloc_getDisplayName */
		intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR,  "name too long");
		RETURN_FALSE;
	}

	if(loc_name_len == 0) {
		loc_name = (char *)intl_locale_get_default();
	}

	if( strcmp(tag_name, DISP_NAME) != 0 ){
		/* Handle grandfathered languages */
		int grOffset = findOffset( LOC_GRANDFATHERED , loc_name );
		if( grOffset >= 0 ){
			if( strcmp(tag_name , LOC_LANG_TAG)==0 ){
				mod_loc_name = getPreferredTag( loc_name );
			} else {
				/* Since Grandfathered, no value, do nothing, return NULL */
				RETURN_FALSE;
			}
		}
	} /* end of if != LOC_CANONICAL_TAG */

	if( mod_loc_name==NULL ){
		mod_loc_name = estrdup( loc_name );
	}

	/* Check if disp_loc_name passed , if not use default locale */
	if( !disp_loc_name){
		disp_loc_name = estrdup(intl_locale_get_default());
		free_loc_name = 1;
	}

	/* Get the disp_value for the given locale */
	do{
		disp_name = reinterpret_cast<UChar *>(erealloc( disp_name , buflen * sizeof(UChar)  ));
		disp_name_len = buflen;

		if( strcmp(tag_name , LOC_LANG_TAG)==0 ){
			buflen = uloc_getDisplayLanguage ( mod_loc_name , disp_loc_name , disp_name , disp_name_len , &status);
		} else if( strcmp(tag_name , LOC_SCRIPT_TAG)==0 ){
			buflen = uloc_getDisplayScript ( mod_loc_name , disp_loc_name , disp_name , disp_name_len , &status);
		} else if( strcmp(tag_name , LOC_REGION_TAG)==0 ){
			buflen = uloc_getDisplayCountry ( mod_loc_name , disp_loc_name , disp_name , disp_name_len , &status);
		} else if( strcmp(tag_name , LOC_VARIANT_TAG)==0 ){
			buflen = uloc_getDisplayVariant ( mod_loc_name , disp_loc_name , disp_name , disp_name_len , &status);
		} else if( strcmp(tag_name , DISP_NAME)==0 ){
			buflen = uloc_getDisplayName ( mod_loc_name , disp_loc_name , disp_name , disp_name_len , &status);
		}

		/* U_STRING_NOT_TERMINATED_WARNING is admissible here; don't look for it */
		if( U_FAILURE( status ) )
		{
			if( status == U_BUFFER_OVERFLOW_ERROR )
			{
				status = U_ZERO_ERROR;
				continue;
			}

			spprintf(&msg, 0, "unable to get locale %s", tag_name );
			intl_error_set( NULL, status, msg);
			efree(msg);
			if( disp_name){
				efree( disp_name );
			}
			if( mod_loc_name){
				efree( mod_loc_name );
			}
			if (free_loc_name) {
				efree((void *)disp_loc_name);
				disp_loc_name = NULL;
			}
			RETURN_FALSE;
		}
	} while( buflen > disp_name_len );

	if( mod_loc_name){
		efree( mod_loc_name );
	}
	if (free_loc_name) {
		efree((void *)disp_loc_name);
		disp_loc_name = NULL;
	}
	/* Convert display locale name from UTF-16 to UTF-8. */
	u8str = intl_convert_utf16_to_utf8(disp_name, buflen, &status );
	efree( disp_name );
	if( !u8str )
	{
		spprintf(&msg, 0, "error converting display name for %s to UTF-8", tag_name );
		intl_error_set( NULL, status, msg);
		efree(msg);
		RETURN_FALSE;
	}

	RETVAL_NEW_STR( u8str );
}
/* }}} */

/* {{{
 * common code shared by display keyword functions to get the value from ICU
 }}} */
static void get_icu_disp_keyword_value_src_php(const char* tag_name, INTERNAL_FUNCTION_PARAMETERS)
{
	char*       loc_name            = NULL;
	size_t      loc_name_len        = 0;
	char*       keyword_name        = NULL;
	size_t      keyword_name_len    = 0;
	char*       disp_loc_name       = NULL;
	size_t      disp_loc_name_len   = 0;
	int         free_loc_name       = 0;

	UChar*      disp_name           = NULL;
	int32_t     disp_name_len       = 0;
	int32_t     buflen              = 512;
	UErrorCode  status              = U_ZERO_ERROR;

	zend_string* u8str;
	char*       msg                 = NULL;

	intl_error_reset( NULL );

	if (strcmp(tag_name, DISP_KEYWORD) == 0) {
		ZEND_PARSE_PARAMETERS_START(1, 2)
			Z_PARAM_PATH(keyword_name, keyword_name_len)
			Z_PARAM_OPTIONAL
			Z_PARAM_PATH_OR_NULL(disp_loc_name, disp_loc_name_len)
		ZEND_PARSE_PARAMETERS_END();
	} else {
		ZEND_PARSE_PARAMETERS_START(2, 3)
			Z_PARAM_PATH(loc_name, loc_name_len)
			Z_PARAM_PATH(keyword_name, keyword_name_len)
			Z_PARAM_OPTIONAL
			Z_PARAM_PATH_OR_NULL(disp_loc_name, disp_loc_name_len)
		ZEND_PARSE_PARAMETERS_END();

		if (UNEXPECTED(loc_name_len > ULOC_FULLNAME_CAPACITY)) {
			intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR, "name too long");
			RETURN_FALSE;
		}

		if (loc_name_len == 0) {
			loc_name = (char *)intl_locale_get_default();
		}
	}

	if (!disp_loc_name) {
		disp_loc_name = estrdup(intl_locale_get_default());
		free_loc_name = 1;
	}

	do {
		disp_name = reinterpret_cast<UChar *>(erealloc(disp_name, buflen * sizeof(UChar)));
		disp_name_len = buflen;

		if (strcmp(tag_name, DISP_KEYWORD) == 0) {
			buflen = uloc_getDisplayKeyword(keyword_name, disp_loc_name, disp_name, disp_name_len, &status);
		} else {
			buflen = uloc_getDisplayKeywordValue(loc_name, keyword_name, disp_loc_name, disp_name, disp_name_len, &status);
		}

		/* U_STRING_NOT_TERMINATED_WARNING is admissible here; don't look for it */
		if (U_FAILURE(status)) {
			if (status == U_BUFFER_OVERFLOW_ERROR) {
				status = U_ZERO_ERROR;
				continue;
			}

			spprintf(&msg, 0, "unable to get locale %s", tag_name);
			intl_error_set( NULL, status, msg);
			efree(msg);
			if (disp_name) {
				efree(disp_name);
			}
			if (free_loc_name) {
				efree((void *)disp_loc_name);
				disp_loc_name = NULL;
			}
			RETURN_FALSE;
		}
	} while (buflen > disp_name_len);

	if (free_loc_name) {
		efree((void *)disp_loc_name);
		disp_loc_name = NULL;
	}

	u8str = intl_convert_utf16_to_utf8(disp_name, buflen, &status);
	efree(disp_name);
	if (!u8str) {
		spprintf(&msg, 0, "error converting display name for %s to UTF-8", tag_name);
		intl_error_set( NULL, status, msg);
		efree(msg);
		RETURN_FALSE;
	}

	RETVAL_NEW_STR(u8str);
}
/* }}} */

/* {{{ gets the name for the $locale in $in_locale or default_locale */
U_CFUNC PHP_FUNCTION(locale_get_display_name)
{
	get_icu_disp_value_src_php( DISP_NAME , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{ gets the language for the $locale in $in_locale or default_locale */
U_CFUNC PHP_FUNCTION(locale_get_display_language)
{
	get_icu_disp_value_src_php( LOC_LANG_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{ gets the script for the $locale in $in_locale or default_locale */
U_CFUNC PHP_FUNCTION(locale_get_display_script)
{
	get_icu_disp_value_src_php( LOC_SCRIPT_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{ gets the region for the $locale in $in_locale or default_locale */
U_CFUNC PHP_FUNCTION(locale_get_display_region)
{
	get_icu_disp_value_src_php( LOC_REGION_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{
* proto static string Locale::getDisplayVariant($locale, $in_locale = null)
* gets the variant for the $locale in $in_locale or default_locale
 }}} */
/* {{{
* proto static string get_display_variant($locale, $in_locale = null)
* gets the variant for the $locale in $in_locale or default_locale
*/
U_CFUNC PHP_FUNCTION(locale_get_display_variant)
{
	get_icu_disp_value_src_php( LOC_VARIANT_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{ gets the keyword display label in $in_locale or default_locale */
U_CFUNC PHP_FUNCTION(locale_get_display_keyword)
{
	get_icu_disp_keyword_value_src_php(DISP_KEYWORD, INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

/* {{{ gets the keyword value display label in $in_locale or default_locale */
U_CFUNC PHP_FUNCTION(locale_get_display_keyword_value)
{
	get_icu_disp_keyword_value_src_php(DISP_KEYWORD_VALUE, INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

 /* {{{ return an associative array containing keyword-value
 * pairs for this locale. The keys are keys to the array (doh!)
 * }}}*/
 /* {{{ return an associative array containing keyword-value
 * pairs for this locale. The keys are keys to the array (doh!)
 */
U_CFUNC PHP_FUNCTION( locale_get_keywords )
{
	UEnumeration*   e        = NULL;
	UErrorCode      status   = U_ZERO_ERROR;

	const char*	 	kw_key        = NULL;
	int32_t         kw_key_len    = 0;

	char*       	        loc_name        = NULL;
	size_t        	 	loc_name_len    = 0;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH(loc_name, loc_name_len)
	ZEND_PARSE_PARAMETERS_END();

	INTL_CHECK_LOCALE_LEN(loc_name_len);

	if(loc_name_len == 0) {
		loc_name = (char *)intl_locale_get_default();
	}

	/* Get the keywords */
	e = uloc_openKeywords( loc_name, &status );
	if( e != NULL ) {
		/*
		ICU expects the buffer to be allocated  before calling the function
		and so the buffer size has been explicitly specified
		ICU uloc.h #define 	ULOC_KEYWORD_AND_VALUES_CAPACITY   100
		hence the kw_value buffer size is 100
		*/

		/* Traverse it, filling the return array. */
		array_init( return_value );

		while( ( kw_key = uenum_next( e, &kw_key_len, &status ) ) != NULL ){
			int32_t kw_value_len = 100;
			zend_string *kw_value_str = zend_string_alloc(kw_value_len, 0);

			/* Get the keyword value for each keyword */
			kw_value_len=uloc_getKeywordValue( loc_name, kw_key, ZSTR_VAL(kw_value_str), kw_value_len, &status );
			if (status == U_BUFFER_OVERFLOW_ERROR) {
				status = U_ZERO_ERROR;
				kw_value_str = zend_string_extend(kw_value_str, kw_value_len, 0);
				kw_value_len=uloc_getKeywordValue( loc_name,kw_key, ZSTR_VAL(kw_value_str), kw_value_len+1, &status );
			} else if(!U_FAILURE(status)) {
				kw_value_str = zend_string_truncate(kw_value_str, kw_value_len, 0);
			}
			if (U_FAILURE(status)) {
				intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR, "Error encountered while getting the keyword value for the keyword");
				if( kw_value_str){
					zend_string_efree( kw_value_str );
				}
				zend_array_destroy(Z_ARR_P(return_value));
				RETURN_FALSE;
			}

			add_assoc_str( return_value, (char *)kw_key, kw_value_str);
		} /* end of while */

	} /* end of if e!=NULL */

	uenum_close( e );
}
/* }}} */

 /* {{{ @return string the canonicalized locale
 * }}} */
 /* {{{ @param string $locale	The locale string to canonicalize */
U_CFUNC PHP_FUNCTION(locale_canonicalize)
{
	get_icu_value_src_php( LOC_CANONICALIZE_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
/* }}} */

/* {{{ append_key_value
* Internal function which is called from locale_compose
* gets the value for the key_name and appends to the loc_name
* returns 1 if successful , -1 if not found ,
* 0 if array element is not a string , -2 if buffer-overflow
*/
static int append_key_value(smart_str* loc_name, HashTable* hash_arr, const char* key_name)
{
	zval *ele_value;

	if ((ele_value = zend_hash_str_find_deref(hash_arr , key_name, strlen(key_name))) != NULL ) {
		if(Z_TYPE_P(ele_value)!= IS_STRING ){
			/* element value is not a string */
			return FAILURE;
		}
		if(strcmp(key_name, LOC_LANG_TAG) != 0 &&
		   strcmp(key_name, LOC_GRANDFATHERED_LANG_TAG)!=0 ) {
			/* not lang or grandfathered tag */
			smart_str_appendl(loc_name, SEPARATOR , sizeof(SEPARATOR)-1);
		}
		smart_str_append(loc_name, Z_STR_P(ele_value));
		return SUCCESS;
	}

	return LOC_NOT_FOUND;
}
/* }}} */

/* {{{ append_prefix , appends the prefix needed
* e.g. private adds 'x'
*/
static void add_prefix(smart_str* loc_name, const char* key_name)
{
	if( strncmp(key_name , LOC_PRIVATE_TAG , 7) == 0 ){
		smart_str_appendl(loc_name, SEPARATOR , sizeof(SEPARATOR)-1);
		smart_str_appendl(loc_name, PRIVATE_PREFIX , sizeof(PRIVATE_PREFIX)-1);
	}
}
/* }}} */

/* {{{ append_multiple_key_values
* Internal function which is called from locale_compose
* gets the multiple values for the key_name and appends to the loc_name
* used for 'variant','extlang','private'
* returns 1 if successful , -1 if not found ,
* 0 if array element is not a string , -2 if buffer-overflow
*/
static int append_multiple_key_values(smart_str* loc_name, HashTable* hash_arr, const char* key_name)
{
	zval	*ele_value;
	int 	isFirstSubtag 	= 0;

	/* Variant/ Extlang/Private etc. */
	if ((ele_value = zend_hash_str_find_deref( hash_arr , key_name , strlen(key_name))) != NULL) {
		if( Z_TYPE_P(ele_value) == IS_STRING ){
			add_prefix( loc_name , key_name);

			smart_str_appendl(loc_name, SEPARATOR , sizeof(SEPARATOR)-1);
			smart_str_append(loc_name, Z_STR_P(ele_value));
			return SUCCESS;
		} else if(Z_TYPE_P(ele_value) == IS_ARRAY ) {
			HashTable *arr = Z_ARRVAL_P(ele_value);
			zval *data;

			ZEND_HASH_FOREACH_VAL(arr, data) {
				ZVAL_DEREF(data);
				if(Z_TYPE_P(data) != IS_STRING) {
					return FAILURE;
				}
				if (isFirstSubtag++ == 0){
					add_prefix(loc_name , key_name);
				}
				smart_str_appendl(loc_name, SEPARATOR , sizeof(SEPARATOR)-1);
				smart_str_append(loc_name, Z_STR_P(data));
			} ZEND_HASH_FOREACH_END();
			return SUCCESS;
		} else {
			return FAILURE;
		}
	} else {
		char cur_key_name[31];
		int  max_value = 0, i;
		/* Decide the max_value: the max. no. of elements allowed */
		if( strcmp(key_name , LOC_VARIANT_TAG) ==0 ){
			max_value  = MAX_NO_VARIANT;
		}
		if( strcmp(key_name , LOC_EXTLANG_TAG) ==0 ){
			max_value  = MAX_NO_EXTLANG;
		}
		if( strcmp(key_name , LOC_PRIVATE_TAG) ==0 ){
			max_value  = MAX_NO_PRIVATE;
		}

		/* Multiple variant values as variant0, variant1 ,variant2 */
		isFirstSubtag = 0;
		for( i=0 ; i< max_value; i++ ){
			snprintf( cur_key_name , 30, "%s%d", key_name , i);
			if ((ele_value = zend_hash_str_find_deref( hash_arr , cur_key_name , strlen(cur_key_name))) != NULL) {
				if( Z_TYPE_P(ele_value)!= IS_STRING ){
					/* variant is not a string */
					return FAILURE;
				}
				/* Add the contents */
				if (isFirstSubtag++ == 0){
					add_prefix(loc_name , cur_key_name);
				}
				smart_str_appendl(loc_name, SEPARATOR , sizeof(SEPARATOR)-1);
				smart_str_append(loc_name, Z_STR_P(ele_value));
			}
		} /* end of for */
	} /* end of else */

	return SUCCESS;
}
/* }}} */

/*{{{
* If applicable sets error message and aborts locale_compose gracefully
* returns 0  if locale_compose needs to be aborted
* otherwise returns 1
*/
static int handleAppendResult( int result, smart_str* loc_name)
{
	intl_error_reset( NULL );
	if( result == FAILURE) {
		intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR,
			 "parameter array element is not a string");
		smart_str_free(loc_name);
		return 0;
	}
	return 1;
}
/* }}} */

#define RETURN_SMART_STR(str) smart_str_0((str)); RETURN_NEW_STR((str)->s)
/* {{{ Creates a locale by combining the parts of locale-ID passed
* }}} */
/* {{{ Creates a locale by combining the parts of locale-ID passed
* }}} */
U_CFUNC PHP_FUNCTION(locale_compose)
{
	smart_str      	loc_name_s = {NULL, 0};
	smart_str *loc_name = &loc_name_s;
	zval*			arr	= NULL;
	HashTable*		hash_arr = NULL;
	int 			result = 0;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_ARRAY(arr)
	ZEND_PARSE_PARAMETERS_END();

	hash_arr = Z_ARRVAL_P( arr );

	if( !hash_arr || zend_hash_num_elements( hash_arr ) == 0 )
		RETURN_FALSE;

	/* Check for grandfathered first */
	result = append_key_value(loc_name, hash_arr,  LOC_GRANDFATHERED_LANG_TAG);
	if( result == SUCCESS){
		RETURN_SMART_STR(loc_name);
	}
	if( !handleAppendResult( result, loc_name)){
		RETURN_FALSE;
	}

	/* Not grandfathered */
	result = append_key_value(loc_name, hash_arr , LOC_LANG_TAG);
	if( result == LOC_NOT_FOUND ){
		zend_argument_value_error(1, "must contain a \"%s\" key", LOC_LANG_TAG);
		smart_str_free(loc_name);
		RETURN_THROWS();
	}
	if( !handleAppendResult( result, loc_name)){
		RETURN_FALSE;
	}

	/* Extlang */
	result = append_multiple_key_values(loc_name, hash_arr , LOC_EXTLANG_TAG);
	if( !handleAppendResult( result, loc_name)){
		RETURN_FALSE;
	}

	/* Script */
	result = append_key_value(loc_name, hash_arr , LOC_SCRIPT_TAG);
	if( !handleAppendResult( result, loc_name)){
		RETURN_FALSE;
	}

	/* Region */
	result = append_key_value( loc_name, hash_arr , LOC_REGION_TAG);
	if( !handleAppendResult( result, loc_name)){
		RETURN_FALSE;
	}

	/* Variant */
	result = append_multiple_key_values( loc_name, hash_arr , LOC_VARIANT_TAG);
	if( !handleAppendResult( result, loc_name)){
		RETURN_FALSE;
	}

	/* Private */
	result = append_multiple_key_values( loc_name, hash_arr , LOC_PRIVATE_TAG);
	if( !handleAppendResult( result, loc_name)){
		RETURN_FALSE;
	}

	RETURN_SMART_STR(loc_name);
}
/* }}} */


/*{{{
* Parses the locale and returns private subtags  if existing
* else returns NULL
* e.g. for locale='en_US-x-prv1-prv2-prv3'
* returns a pointer to the string 'prv1-prv2-prv3'
*/
static zend_string* get_private_subtags(const char* loc_name)
{
	zend_string* result = NULL;
	size_t       len = 0;
	const char*  mod_loc_name =NULL;

	if( loc_name && (len = strlen(loc_name)) > 0 ){
		zend_off_t singletonPos = 0;
		mod_loc_name = loc_name ;
		while( (singletonPos = getSingletonPos(mod_loc_name)) > -1){
			if( (*(mod_loc_name+singletonPos)=='x') || (*(mod_loc_name+singletonPos)=='X') ){
				/* private subtag start found */
				if( singletonPos + 2 ==  len){
					/* loc_name ends with '-x-' ; return  NULL */
				}
				else{
					/* result = mod_loc_name + singletonPos +2; */
					result = zend_string_init(mod_loc_name + singletonPos+2  , (len -( singletonPos +2) ), 0);
				}
				break;
			}
			else{
				if((size_t)(singletonPos + 1) >= len){
					/* String end */
					break;
				} else {
					/* singleton found but not a private subtag , hence check further in the string for the private subtag */
					mod_loc_name = mod_loc_name + singletonPos +1;
					len = strlen(mod_loc_name);
				}
			}
		} /* end of while */
	}

	return result;
}
/* }}} */

/* {{{ code used by locale_parse */
static int add_array_entry(const char* loc_name, zval* hash_arr, const char* key_name)
{
	zend_string*   key_value 	= NULL;
	char*   cur_key_name	= NULL;
	char*   token        	= NULL;
	char*   last_ptr  	= NULL;

	int	result		= 0;
	int 	cur_result  	= 0;


	if( strcmp(key_name , LOC_PRIVATE_TAG)==0 ){
		key_value = get_private_subtags( loc_name );
		result = 1;
	} else {
		key_value = get_icu_value_internal( loc_name , key_name , &result,1 );
	}
	if( (strcmp(key_name , LOC_PRIVATE_TAG)==0) ||
		( strcmp(key_name , LOC_VARIANT_TAG)==0) ){
		if( result > 0 && key_value){
			int cnt = 0;
			/* Tokenize on the "_" or "-"  */
			token = php_strtok_r( key_value->val , DELIMITER ,&last_ptr);
			if( cur_key_name ){
				efree( cur_key_name);
			}
			/* Over-allocates a few bytes for the integer so we don't have to reallocate. */
			size_t cur_key_name_size = (sizeof("-2147483648") - 1) + strlen(key_name) + 1;
			cur_key_name = reinterpret_cast<char *>(emalloc(cur_key_name_size));
			snprintf( cur_key_name, cur_key_name_size , "%s%d", key_name , cnt++);
			add_assoc_string( hash_arr, cur_key_name , token);
			/* tokenize on the "_" or "-" and stop  at singleton if any */
			while( (token = php_strtok_r(NULL , DELIMITER , &last_ptr)) && (strlen(token)>1) ){
				snprintf( cur_key_name , cur_key_name_size, "%s%d", key_name , cnt++);
				add_assoc_string( hash_arr, cur_key_name , token);
			}
/*
			if( strcmp(key_name, LOC_PRIVATE_TAG) == 0 ){
			}
*/
		}
		if (key_value) {
			zend_string_release_ex(key_value, 0);
		}
	} else {
		if( result == 1 ){
			add_assoc_str( hash_arr, key_name , key_value);
			cur_result = 1;
		} else if (key_value) {
			zend_string_release_ex(key_value, 0);
		}
	}

	if( cur_key_name ){
		efree( cur_key_name);
	}
	/*if( key_name != LOC_PRIVATE_TAG && key_value){*/
	return cur_result;
}
/* }}} */

/* {{{ parses a locale-id into an array the different parts of it */
U_CFUNC PHP_FUNCTION(locale_parse)
{
	char*          loc_name        = NULL;
	size_t         loc_name_len    = 0;
	int         grOffset    	= 0;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH(loc_name, loc_name_len)
	ZEND_PARSE_PARAMETERS_END();

	INTL_CHECK_LOCALE_LEN(loc_name_len);

	if(loc_name_len == 0) {
		loc_name = (char *)intl_locale_get_default();
	}

	array_init( return_value );

	grOffset =  findOffset( LOC_GRANDFATHERED , loc_name );
	if( grOffset >= 0 ){
		add_assoc_string( return_value , LOC_GRANDFATHERED_LANG_TAG, (char *)loc_name);
	}
	else{
		/* Not grandfathered */
		add_array_entry( loc_name , return_value , LOC_LANG_TAG);
		add_array_entry( loc_name , return_value , LOC_SCRIPT_TAG);
		add_array_entry( loc_name , return_value , LOC_REGION_TAG);
		add_array_entry( loc_name , return_value , LOC_VARIANT_TAG);
		add_array_entry( loc_name , return_value , LOC_PRIVATE_TAG);
	}
}
/* }}} */

/* {{{ gets an array containing the list of variants, or null */
U_CFUNC PHP_FUNCTION(locale_get_all_variants)
{
	char*  	                loc_name        = NULL;
	size_t    		loc_name_len    = 0;

	int	result		= 0;
	char*	token		= NULL;
	zend_string*	variant		= NULL;
	char*	saved_ptr	= NULL;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH(loc_name, loc_name_len)
	ZEND_PARSE_PARAMETERS_END();

	if(loc_name_len == 0) {
		loc_name = (char *)intl_locale_get_default();
		loc_name_len = strlen(loc_name);
	}

	INTL_CHECK_LOCALE_LEN(loc_name_len);

	array_init( return_value );

	/* If the locale is grandfathered, stop, no variants */
	if( findOffset( LOC_GRANDFATHERED , loc_name ) >=  0 ){
		/* ("Grandfathered Tag. No variants."); */
	}
	else {
	/* Call ICU variant */
		variant = get_icu_value_internal( loc_name , LOC_VARIANT_TAG , &result ,0);
		if( result > 0 && variant){
			/* Tokenize on the "_" or "-" */
			token = php_strtok_r( variant->val , DELIMITER , &saved_ptr);
			add_next_index_stringl( return_value, token , strlen(token));
			/* tokenize on the "_" or "-" and stop  at singleton if any	*/
			while( (token = php_strtok_r(NULL , DELIMITER, &saved_ptr)) && (strlen(token)>1) ){
				add_next_index_stringl( return_value, token , strlen(token));
			}
		}
		if( variant ){
			zend_string_release_ex( variant, 0 );
		}
	}


}
/* }}} */

/* {{{ Converts to lower case and also replaces all hyphens with the underscore */
static int strToMatch(const char* str ,char *retstr)
{
	char* 	anchor 	= NULL;
	const char* 	anchor1 = NULL;
	int 	result 	= 0;

	if( (!str) || str[0] == '\0'){
		return result;
	} else {
		anchor = retstr;
		anchor1 = str;
			while( (*str)!='\0' ){
			if( *str == '-' ){
				*retstr =  '_';
			} else {
				*retstr = tolower((unsigned char)*str);
			}
				str++;
				retstr++;
		}
		*retstr = '\0';
		retstr=  anchor;
		str=  anchor1;
		result = 1;
	}

	return(result);
}
/* }}} */

/* {{{ Checks if a $langtag filter matches with $locale according to RFC 4647's basic filtering algorithm */
/* }}} */
/* {{{ Checks if a $langtag filter matches with $locale according to RFC 4647's basic filtering algorithm */
U_CFUNC PHP_FUNCTION(locale_filter_matches)
{
	char*       	lang_tag        = NULL;
	size_t         	lang_tag_len    = 0;
	char*           loc_range       = NULL;
	size_t         	loc_range_len   = 0;

	int		result		= 0;
	char*		token		= 0;
	char*		chrcheck	= NULL;

	zend_string*   	can_lang_tag    = NULL;
	zend_string*   	can_loc_range   = NULL;

	char*       	cur_lang_tag    = NULL;
	char*       	cur_loc_range   = NULL;

	bool 	boolCanonical 	= 0;
	UErrorCode	status		= U_ZERO_ERROR;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(2, 3)
		Z_PARAM_PATH(lang_tag, lang_tag_len)
		Z_PARAM_PATH(loc_range,  loc_range_len)
		Z_PARAM_OPTIONAL
		Z_PARAM_BOOL(boolCanonical)
	ZEND_PARSE_PARAMETERS_END();

	if(loc_range_len == 0) {
		loc_range = (char *)intl_locale_get_default();
		loc_range_len = strlen(loc_range);
	}

	if( strcmp(loc_range,"*")==0){
		RETURN_TRUE;
	}

	INTL_CHECK_LOCALE_LEN(loc_range_len);
	INTL_CHECK_LOCALE_LEN(lang_tag_len);

	if( boolCanonical ){
		/* canonicalize loc_range */
		can_loc_range=get_icu_value_internal( loc_range , LOC_CANONICALIZE_TAG , &result , 0);
		if( result <=0) {
			intl_error_set(NULL, status, "unable to canonicalize loc_range");
			RETURN_FALSE;
		}

		/* canonicalize lang_tag */
		can_lang_tag = get_icu_value_internal( lang_tag , LOC_CANONICALIZE_TAG , &result ,  0);
		if( result <=0) {
			zend_string_release_ex( can_loc_range, false );
			intl_error_set(NULL, status, "unable to canonicalize lang_tag");
			RETURN_FALSE;
		}

		/* Convert to lower case for case-insensitive comparison */
		cur_lang_tag = reinterpret_cast<char *>(ecalloc( 1, can_lang_tag->len + 1));

		/* Convert to lower case for case-insensitive comparison */
		result = strToMatch( can_lang_tag->val , cur_lang_tag);
		if( result == 0) {
			efree( cur_lang_tag );
			zend_string_release_ex( can_lang_tag, 0 );
			RETURN_FALSE;
		}

		cur_loc_range = reinterpret_cast<char *>(ecalloc( 1, can_loc_range->len + 1));
		result = strToMatch( can_loc_range->val , cur_loc_range );
		if( result == 0) {
			efree( cur_lang_tag );
			zend_string_release_ex( can_lang_tag, 0 );
			efree( cur_loc_range );
			zend_string_release_ex( can_loc_range, 0 );
			RETURN_FALSE;
		}

		/* check if prefix */
		token 	= strstr( cur_lang_tag , cur_loc_range );

		if( token && (token==cur_lang_tag) ){
			/* check if the char. after match is SEPARATOR */
			chrcheck = token + can_loc_range->len;
			if( isIDSeparator(*chrcheck) || isKeywordSeparator(*chrcheck) || isEndOfTag(*chrcheck) ){
				efree( cur_lang_tag );
				efree( cur_loc_range );
				if( can_lang_tag){
					zend_string_release_ex( can_lang_tag, 0 );
				}
				if( can_loc_range){
					zend_string_release_ex( can_loc_range, 0 );
				}
				RETURN_TRUE;
			}
		}

		/* No prefix as loc_range */
		if( cur_lang_tag){
			efree( cur_lang_tag );
		}
		if( cur_loc_range){
			efree( cur_loc_range );
		}
		if( can_lang_tag){
			zend_string_release_ex( can_lang_tag, 0 );
		}
		if( can_loc_range){
			zend_string_release_ex( can_loc_range, 0 );
		}
		RETURN_FALSE;

	} /* end of if isCanonical */
	else{
		/* Convert to lower case for case-insensitive comparison */
		cur_lang_tag = reinterpret_cast<char *>(ecalloc(1, lang_tag_len + 1));

		result = strToMatch( lang_tag , cur_lang_tag);
		if( result == 0) {
			efree( cur_lang_tag );
			RETURN_FALSE;
		}
		cur_loc_range = reinterpret_cast<char *>(ecalloc(1, loc_range_len + 1));
		result = strToMatch( loc_range , cur_loc_range );
		if( result == 0) {
			efree( cur_lang_tag );
			efree( cur_loc_range );
			RETURN_FALSE;
		}

		/* check if prefix */
		token 	= strstr( cur_lang_tag , cur_loc_range );

		if( token && (token==cur_lang_tag) ){
			/* check if the char. after match is SEPARATOR */
			chrcheck = token + loc_range_len;
			if( isIDSeparator(*chrcheck) || isEndOfTag(*chrcheck) ){
				efree( cur_lang_tag );
				efree( cur_loc_range );
				RETURN_TRUE;
			}
		}

		/* No prefix as loc_range */
		if( cur_lang_tag){
			efree( cur_lang_tag );
		}
		if( cur_loc_range){
			efree( cur_loc_range );
		}
		RETURN_FALSE;

	}
}
/* }}} */

static void array_cleanup( char* arr[] , int arr_size)
{
	int i=0;
	for( i=0; i< arr_size; i++ ){
		if( arr[i*2] ){
			efree( arr[i*2]);
		}
	}
	efree(arr);
}

#define LOOKUP_CLEAN_RETURN(value)	array_cleanup(cur_arr, cur_arr_len); return (value)
/* {{{
* returns the lookup result to lookup_loc_range_src_php
* internal function
*/
static zend_string* lookup_loc_range(const char* loc_range, HashTable* hash_arr, int canonicalize )
{
	int	i = 0;
	int	cur_arr_len = 0;
	int result = 0;

	zend_string* lang_tag = NULL;
	zval* ele_value = NULL;

	char* cur_loc_range	= NULL;
	zend_string* can_loc_range	= NULL;
	zend_off_t saved_pos = 0;

	zend_string* return_value = NULL;

	char **cur_arr = reinterpret_cast<char **>(ecalloc(zend_hash_num_elements(hash_arr)*2, sizeof(char *)));
	ZEND_HASH_FOREACH_VAL(hash_arr, ele_value) {
		ZVAL_DEREF(ele_value);
	/* convert the array to lowercase , also replace hyphens with the underscore and store it in cur_arr */
		if(Z_TYPE_P(ele_value)!= IS_STRING) {
			/* element value is not a string */
			zend_argument_type_error(2, "must only contain string values");
			LOOKUP_CLEAN_RETURN(NULL);
		}
		if (zend_str_has_nul_byte(Z_STR_P(ele_value))) {
			zend_argument_value_error(2, "must not contain any null bytes");
			LOOKUP_CLEAN_RETURN(NULL);
		}
		i = cur_arr_len*2;
		cur_arr[i] = estrndup(Z_STRVAL_P(ele_value), Z_STRLEN_P(ele_value));
		cur_arr_len++;
		result = strToMatch(Z_STRVAL_P(ele_value), cur_arr[i]);
		if(result == 0) {
			intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "unable to canonicalize lang_tag");
			LOOKUP_CLEAN_RETURN(NULL);
		}
		cur_arr[i+1] = Z_STRVAL_P(ele_value);
	} ZEND_HASH_FOREACH_END(); /* end of for */

	/* Canonicalize array elements */
	if(canonicalize) {
		for(i=0; i<cur_arr_len; i++) {
			lang_tag = get_icu_value_internal(cur_arr[i*2], LOC_CANONICALIZE_TAG, &result, 0);
			if(result != 1 || lang_tag == NULL || !lang_tag->val[0]) {
				if(lang_tag) {
					zend_string_release_ex(lang_tag, 0);
				}
				intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "unable to canonicalize lang_tag");
				LOOKUP_CLEAN_RETURN(NULL);
			}
			cur_arr[i*2] = reinterpret_cast<char *>(erealloc(cur_arr[i*2], lang_tag->len+1));
			result = strToMatch(lang_tag->val, cur_arr[i*2]);
			zend_string_release_ex(lang_tag, 0);
			if(result == 0) {
				intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "unable to canonicalize lang_tag");
				LOOKUP_CLEAN_RETURN(NULL);
			}
		}

	}

	if(canonicalize) {
		/* Canonicalize the loc_range */
		can_loc_range = get_icu_value_internal(loc_range, LOC_CANONICALIZE_TAG, &result , 0);
		if( result != 1 || can_loc_range == NULL || !can_loc_range->val[0]) {
			/* Error */
			intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "unable to canonicalize loc_range");
			if(can_loc_range) {
				zend_string_release_ex(can_loc_range, 0);
			}
			LOOKUP_CLEAN_RETURN(NULL);
		} else {
			loc_range = can_loc_range->val;
		}
	}

	cur_loc_range = reinterpret_cast<char *>(ecalloc(1, strlen(loc_range)+1));
	/* convert to lower and replace hyphens */
	result = strToMatch(loc_range, cur_loc_range);
	if(can_loc_range) {
		zend_string_release_ex(can_loc_range, 0);
	}
	if(result == 0) {
		efree(cur_loc_range);
		intl_error_set(NULL, U_ILLEGAL_ARGUMENT_ERROR, "unable to canonicalize lang_tag");
		LOOKUP_CLEAN_RETURN(NULL);
	}

	/* Lookup for the lang_tag match */
	saved_pos = strlen(cur_loc_range);
	while(saved_pos > 0) {
		for(i=0; i< cur_arr_len; i++){
			if(cur_arr[i*2] != NULL && strlen(cur_arr[i*2]) == saved_pos && strncmp(cur_loc_range, cur_arr[i*2], saved_pos) == 0) {
				/* Match found */
				char *str = canonicalize ? cur_arr[i*2] : cur_arr[i*2+1];
				return_value = zend_string_init(str, strlen(str), 0);
				efree(cur_loc_range);
				LOOKUP_CLEAN_RETURN(return_value);
			}
		}
		saved_pos = getStrrtokenPos(cur_loc_range, saved_pos);
	}

	/* Match not found */
	efree(cur_loc_range);
	LOOKUP_CLEAN_RETURN(NULL);
}
/* }}} */

/* {{{ Searches the items in $langtag for the best match to the language
* range
*/
/* }}} */
/* {{{ Searches the items in $langtag for the best match to the language
* range
*/
U_CFUNC PHP_FUNCTION(locale_lookup)
{
	zend_string*   	fallback_loc_str	= NULL;
	char*    	loc_range      		= NULL;
	size_t        	loc_range_len  		= 0;

	zval*		arr				= NULL;
	HashTable*	hash_arr		= NULL;
	bool	boolCanonical	= 0;
	zend_string* 	result_str	= NULL;

	intl_error_reset( NULL );

	ZEND_PARSE_PARAMETERS_START(2, 4)
		Z_PARAM_ARRAY(arr)
		Z_PARAM_PATH(loc_range, loc_range_len)
		Z_PARAM_OPTIONAL
		Z_PARAM_BOOL(boolCanonical)
		Z_PARAM_PATH_STR_OR_NULL(fallback_loc_str)
	ZEND_PARSE_PARAMETERS_END();

	if(loc_range_len == 0) {
		if(fallback_loc_str) {
			loc_range = ZSTR_VAL(fallback_loc_str);
			loc_range_len = ZSTR_LEN(fallback_loc_str);
		} else {
			loc_range = (char *)intl_locale_get_default();
			loc_range_len = strlen(loc_range);
		}
	}

	hash_arr = Z_ARRVAL_P(arr);

	INTL_CHECK_LOCALE_LEN(loc_range_len);

	if( !hash_arr || zend_hash_num_elements( hash_arr ) == 0 ) {
		RETURN_EMPTY_STRING();
	}

	result_str = lookup_loc_range(loc_range, hash_arr, boolCanonical);
	if (EG(exception)) {
		RETURN_THROWS();
	}
	if (U_FAILURE(intl_error_get_code(NULL))) {
		if (result_str) {
			zend_string_release_ex(result_str, 0);
		}
		RETURN_NULL();
	}
	if(result_str == NULL || ZSTR_VAL(result_str)[0] == '\0') {
		if( fallback_loc_str ) {
			result_str = zend_string_copy(fallback_loc_str);
		} else {
			RETURN_EMPTY_STRING();
		}
	}

	RETURN_STR(result_str);
}
/* }}} */

/* {{{ Tries to find out best available locale based on HTTP "Accept-Language" header */
U_CFUNC PHP_FUNCTION(locale_accept_from_http)
{
	UEnumeration *available;
	char *http_accept = NULL;
	size_t http_accept_len;
	UErrorCode status = U_ZERO_ERROR;
	int len;
	char resultLocale[INTL_MAX_LOCALE_LEN+1];
	UAcceptResult outResult;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_STRING(http_accept, http_accept_len)
	ZEND_PARSE_PARAMETERS_END();
	if (UNEXPECTED(http_accept_len > ULOC_FULLNAME_CAPACITY)) {
		/* check each fragment, if any bigger than capacity, can't do it due to bug #72533 */
		char *start = http_accept;
		char *end;
		size_t len;
		do {
			end = strchr(start, ',');
			len = end ? end-start : http_accept_len-(start-http_accept);
			if (UNEXPECTED(len > ULOC_FULLNAME_CAPACITY)) {
				intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR,
						"locale string too long");
				RETURN_FALSE;
			}
			if(end) {
				start = end+1;
			}
		} while(end != NULL);
	}

	available = ures_openAvailableLocales(NULL, &status);
	INTL_CHECK_STATUS(status, "failed to retrieve locale list");
	len = uloc_acceptLanguageFromHTTP(resultLocale, INTL_MAX_LOCALE_LEN,
						&outResult, http_accept, available, &status);
	uenum_close(available);
	INTL_CHECK_STATUS(status, "failed to find acceptable locale");
	if (UNEXPECTED(len < 0 || outResult == ULOC_ACCEPT_FAILED)) {
		RETURN_FALSE;
	}
	RETURN_STRINGL(resultLocale, len);
}
/* }}} */

U_CFUNC PHP_FUNCTION(locale_is_right_to_left)
{
	char *locale;
	size_t locale_len;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH(locale, locale_len)
	ZEND_PARSE_PARAMETERS_END();

	if (!locale_len) {
		locale = (char *)intl_locale_get_default();
	}

	RETURN_BOOL(uloc_isRightToLeft(locale));
}

U_CFUNC PHP_FUNCTION(locale_add_likely_subtags)
{
	char *locale, maximized_locale[ULOC_FULLNAME_CAPACITY];
	UErrorCode status = U_ZERO_ERROR;
	size_t locale_len;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH(locale, locale_len)
	ZEND_PARSE_PARAMETERS_END();

	if (!locale_len) {
		locale = (char *)intl_locale_get_default();
	}

	const int32_t maximized_locale_len = uloc_addLikelySubtags(locale, maximized_locale, sizeof(maximized_locale), &status);
	INTL_CHECK_STATUS(status, "invalid locale");
	if (UNEXPECTED(maximized_locale_len < 0)) {
		RETURN_FALSE;
	}

	RETURN_STRINGL(maximized_locale, maximized_locale_len);
}

U_CFUNC PHP_FUNCTION(locale_minimize_subtags)
{
	char *locale, minimized_locale[ULOC_FULLNAME_CAPACITY];
	UErrorCode status = U_ZERO_ERROR;
	size_t locale_len;

	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_PATH(locale, locale_len)
	ZEND_PARSE_PARAMETERS_END();

	if (!locale_len) {
		locale = (char *)intl_locale_get_default();
	}

	const int32_t minimized_locale_len = uloc_minimizeSubtags(locale, minimized_locale, sizeof(minimized_locale), &status);
	INTL_CHECK_STATUS(status, "invalid locale");
	if (UNEXPECTED(minimized_locale_len < 0)) {
		RETURN_FALSE;
	}

	RETURN_STRINGL(minimized_locale, minimized_locale_len);
}
