#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "Zend/zend_exceptions.h"
#include "php_bbcode.h"

/* XXX: only required for php_debug_zval_dump */
#include "ext/standard/php_var.h"

/* XXX: required for pcre_cache_entry */
#include "ext/pcre/php_pcre.h"

/* If you declare any globals in php_bbcode.h uncomment this:
ZEND_DECLARE_MODULE_GLOBALS(bbcode)
*/

typedef struct _bbcode_object {
	HashTable *tag;
	HashTable *smiley;
	zend_string *root;
	zend_long flag;
	zend_object std;
} bbcode_object;

/* True global resources - no need for thread safety here */
zend_object_handlers bbcode_handlers;

/* BBCode object */
zend_class_entry *bbcode_ce;

/* BBCode exception object */
zend_class_entry *bbcode_exception_ce;

/* Define all constants */
#ifdef old_code_dropped
	GEN(TYPE_ARG) /*BBCODE_TYPE_ARG*/\
	GEN(TYPE_NOARG) /*BBCODE_TYPE_NOARG*/\
	GEN(TYPE_OPTARG) /*BBCODE_TYPE_OPTARG*/\

#endif

#define BBCODE_DEF(GEN) \
	/* Types */ \
	GEN(TYPE_ROOT) /*BBCODE_TYPE_ROOT*/\
	GEN(TYPE_MULTI) /*BBCODE_TYPE_ARG|BBCODE_TYPE_OPTARG|BBCODE_TYPE_NOARG*/\
	GEN(TYPE_SINGLE) /*BBCODE_TYPE_SINGLE*/\

/* TODO: add again each feature when implemented (before beta) */
#if 0
	/* Quotes */ \
	GEN(QUOTE_DOUBLE) /*BBCODE_ARG_DOUBLE_QUOTE*/\
	GEN(QUOTE_SIMPLE) /*BBCODE_ARG_SINGLE_QUOTE*/\
	GEN(QUOTE_HTML) /*BBCODE_ARG_HTML_QUOTE*/\
	GEN(QUOTE_ESCAPE) /*BBCODE_ARG_QUOTE_ESCAPING*/\
	\
	/* Corrections */ \
	GEN(CORRECT_AUTO) /*BBCODE_AUTO_CORRECT*/\
	GEN(CORRECT_REOPEN) /*BBCODE_CORRECT_REOPEN_TAGS*/\
	GEN(CORRECT_NOTREE) /*BBCODE_DISABLE_TREE_BUILD*/\
	\
	/* Smileys */ \
	GEN(SMILEY_ON) /*BBCODE_DEFAULT_SMILEYS_ON*/\
	GEN(SMILEY_CI) /*BBCODE_SMILEYS_CASE_INSENSITIVE*/\
	\
	/* Flags */ \
	GEN(CLOSE_AUTO) /*BBCODE_FLAGS_ONE_OPEN_PER_LEVEL*/\
	GEN(REMOVE_EMPTY) /*BBCODE_FLAGS_REMOVE_IF_EMPTY*/\
	GEN(REMOVE_CONTENT) /*BBCODE_FLAGS_CDATA_NOT_ALLOWED*/\
	GEN(REMOVE_REOPEN) /*BBCODE_FLAGS_DENY_REOPEN_CHILD*/
#endif


/* Generate enum version */
#define BBCODE_GEN_ENUM(NAME) BBCODE_ ## NAME,

/* Generate zend const version */
#define BBCODE_GEN_CONST(NAME) zend_declare_class_constant_long(bbcode_ce, #NAME, sizeof(#NAME) - 1, BBCODE_ ## NAME TSRMLS_CC);

/* Defined in /usr/include/php/Zend/zend_types.h +382 */
#define BBCODE_GET_TYPE(TYPE) (TYPE==IS_UNDEF)?"undef":( \
			(TYPE==IS_NULL)?"null":( \
				(TYPE==IS_FALSE)?"false":( \
					(TYPE==IS_TRUE)?"true":( \
						(TYPE==IS_LONG)?"long":( \
							(TYPE==IS_DOUBLE)?"double":( \
								(TYPE==IS_STRING)?"string":( \
									(TYPE==IS_ARRAY)?"array":( \
										(TYPE==IS_OBJECT)?"object":( \
											(TYPE==IS_RESOURCE)?"resource":( \
												(TYPE==IS_REFERENCE)?"reference":( \
													(TYPE==IS_CONSTANT_AST)?"constant":"unknown"\
												) \
											) \
										) \
									) \
								) \
							) \
						) \
					) \
				) \
			) \
		)

/* All flags enum set */
enum {
	BBCODE_DEF(BBCODE_GEN_ENUM)
};

/* Set default flag 
 * TODO: Implement it in an ini file ? (later maybe) */
#define BBCODE_DEFAULT_FLAG 0
#if 0
#define BBCODE_DEFAULT_FLAG BBCODE_QUOTE_DOUBLE|BBCODE_QUOTE_SIMPLE|BBCODE_QUOTE_ESCAPE|BBCODE_CORRECT_AUTO|BBCODE_CORRECT_REOPEN|BBCODE_SMILEY_ON|BBCODE_CLOSE_AUTO|BBCODE_REMOVE_EMPTY
#endif

/* Set debug flag */
#define BBCODE_DEBUG 0

/* {{{ Function prototypes (bbcode_destroy|bbcode_free|bbcode_clone|bbcode_create) */
static void bbcode_destroy(zend_object *obj TSRMLS_DC);
static void bbcode_free(zend_object *obj TSRMLS_DC);
static zend_object *bbcode_clone(zval *obj TSRMLS_DC);
static zend_object *bbcode_create(zend_class_entry *ce TSRMLS_DC);
/* }}} */

/* {{{ static inline bbcode_object* bbcode_fetch(zend_object *obj TSRMLS_DC) {
 * BBCode object fetch function */
static inline bbcode_object* bbcode_fetch(zend_object *obj TSRMLS_DC) {
	return (bbcode_object *)((char *)obj - XtOffsetOf(bbcode_object, std));
}
/* }}} */

/* {{{ Z_BBCODE_P(zv) bbcode_fetch(Z_OBJ_P((zv)))
 * BBCode object pointer fetch macro */
#define Z_BBCODE_P(zv) bbcode_fetch(Z_OBJ_P((zv)))
/* }}} */

/* {{{ static void bbcode_destroy(zend_object *obj TSRMLS_DC) {
 * BBCode object destroy call */
static void bbcode_destroy(zend_object *obj TSRMLS_DC) {
	/* Trigger user land destructor on obj */
	zend_objects_destroy_object(obj);
}
/* }}} */

/* {{{ static void bbcode_free(zend_object *obj TSRMLS_DC) {
 * BBCode object free call */
static void bbcode_free(zend_object *obj TSRMLS_DC) {
	/* Retrieve bbcode object from zend object */
	bbcode_object *bbobj = bbcode_fetch(obj);

	/* Check if root is present */
	if (bbobj->root) {
		/* Release the root string */
		zend_string_release(bbobj->root);
	}

	/* Check if tag is present */
	if (bbobj->tag) {
		/* Release the tag hash */
		zend_array_destroy(bbobj->tag);
	}

	/* Check if smiley is present */
	if (bbobj->smiley) {
		/* Release the smiley hash */
		zend_array_destroy(bbobj->smiley);
	}

	/* Trigger zend object std destructor on obj */
	zend_object_std_dtor(obj);

	/* Free the bbobj */
	efree(bbobj);
}
/* }}} */

/* {{{ static zend_object *bbcode_clone(zval *obj TSRMLS_DC) {
 * BBCode object clone call */
static zend_object *bbcode_clone(zval *obj TSRMLS_DC) {
	/* Fetch pointer to old bbcode object */
	bbcode_object *oldbbobj = Z_BBCODE_P(obj);

	/* Create pointer on new obj */
	//bbcode_object *newbbobj = bbcode_fetch(bbcode_create(oldbbobj->std.ce));
	bbcode_object *newbbobj = bbcode_fetch(bbcode_create(Z_OBJ_P(obj)->ce));

	/* Call members cloning function */
	zend_objects_clone_members(&newbbobj->std, &oldbbobj->std);

	/* Duplicate flag value */
	newbbobj->flag = oldbbobj->flag;

	/* Set root as null */
	newbbobj->root = NULL;

	/* Check if root is available */
	if (oldbbobj->root) {
		/* Duplicate root zend string */
		newbbobj->root = zend_string_copy(oldbbobj->root);
	}

	/* Set tag as null */
	newbbobj->tag = NULL;

	/* Check if tag is available */
	if (oldbbobj->tag) {
		/* Duplicate tag zend array */
		newbbobj->tag = zend_array_dup(oldbbobj->tag);
	}

	/* Set smiley as null */
	newbbobj->smiley = NULL;

	/* Check if smiley is available */
	if (oldbbobj->smiley) {
		/* Duplicate smiley zend array */
		newbbobj->smiley = zend_array_dup(oldbbobj->smiley);
	}

	/* Return the new object */
	return &newbbobj->std;
}
/* }}} */

/* {{{ static zend_object *bbcode_create(zend_class_entry *ce TSRMLS_DC) {
 * BBCode object create call */
static zend_object *bbcode_create(zend_class_entry *ce TSRMLS_DC) {
	/* Allocate object */
	bbcode_object *obj = (bbcode_object *)ecalloc(1, sizeof(bbcode_object) + zend_object_properties_size(ce));

	/* Init std member */
	zend_object_std_init(&obj->std, ce TSRMLS_CC);

	/* Set object properties */
	object_properties_init(&obj->std, ce TSRMLS_CC);

	/* Duplicate standard object handler */
	memcpy(&bbcode_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));

	/* Set object offset */
	bbcode_handlers.offset = XtOffsetOf(bbcode_object, std);

	/* Set destructor function */
	bbcode_handlers.dtor_obj = (zend_object_dtor_obj_t) bbcode_destroy;

	/* Set free function */
	bbcode_handlers.free_obj = (zend_object_free_obj_t) bbcode_free;

	/* Set clone function */
	bbcode_handlers.clone_obj = (zend_object_clone_obj_t) bbcode_clone;

	/* Assign custom handler */
	obj->std.handlers = &bbcode_handlers;

	/* Init tag */
	obj->tag = NULL;

	/* Init smiley */
	obj->smiley = NULL;

	/* Init root */
	obj->root = NULL;

	/* Init flag */
	obj->flag = BBCODE_DEFAULT_FLAG;

	/* Return the std member address */
	return &obj->std;
}
/* }}} */

/* {{{ ZEND_BEGIN_ARG_INFO_EX(bbcode_construct_arginfo) */
// ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args)
ZEND_BEGIN_ARG_INFO_EX(bbcode_construct_arginfo, 0, 0, 1)
	// ZEND_ARG_ARRAY_INFO(pass_by_ref, name, allow_null)
	ZEND_ARG_ARRAY_INFO(0, tag, 0)
	ZEND_ARG_ARRAY_INFO(0, smiley, 1)
	// ZEND_ARG_INFO(pass_by_ref, name)
	ZEND_ARG_INFO(0, flag)
ZEND_END_ARG_INFO()
/* }}} */

/* {{{ ZEND_BEGIN_ARG_INFO_EX(bbcode_parse_arginfo) */
// ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args)
ZEND_BEGIN_ARG_INFO_EX(bbcode_parse_arginfo, 0, 0, 1)
	// ZEND_ARG_INFO(pass_by_ref, name)
	ZEND_ARG_INFO(0, str)
ZEND_END_ARG_INFO()
/* }}} */

/* {{{ static HashTable *bbcode_gen_child(HashTable *ret, HashTable *tag, zend_string *cur TSRMLS_DC) { */
static HashTable *bbcode_gen_child(HashTable *ret, HashTable *tag, zend_string *cur TSRMLS_DC) {
	/* Key string */
	zend_string *key;
	/* Value */
	zval *val;

	/* Iterate on each key val until hash end is reached */
	ZEND_HASH_FOREACH_STR_KEY_VAL(tag, key, val) {
		/* Check if key is not a string, value is null or not an array */
		if (key == NULL || val == NULL || Z_TYPE_P(val) != IS_ARRAY) {
			/* Return failure, useless to generate a hash when we will trigger an error later */
			return NULL;
		}

		/* Init type val */
		zval *type;

		/* Check that we have no type or that it's not root type */
		/* XXX: it's not possible to have in child the root tag */
		if ((type = zend_hash_str_find(Z_ARR_P(val), "type", strlen("type"))) == NULL || Z_LVAL_P(type) != BBCODE_TYPE_ROOT) {
			/* Check that we are not on cur key */
			/* XXX: not really required, but it's stupid to allow same tag in child */
			//if (strcmp(ZSTR_VAL(cur), ZSTR_VAL(key)) != 0) {
				/* Init parent val */
				zval *parent;

				/* Init toInsert bool */
				unsigned char toInsert = 0;

				/* Check if we find parent key */
				if ((parent = zend_hash_str_find(Z_ARR_P(val), "parent", strlen("parent"))) != NULL) {
					/* Parent value */
					zval *pval;

					/* Iterate on each val until hash end is reached */
					ZEND_HASH_FOREACH_VAL(Z_ARR_P(parent), pval) {
						/* Check that parent val is a string */
						if (Z_TYPE_P(pval) != IS_STRING) {
							return NULL;
						}

						/* Check that tkey is a string and is identical */
						if (strcmp(Z_STRVAL_P(pval), ZSTR_VAL(cur)) == 0) {
							toInsert = 1;
						}
					} ZEND_HASH_FOREACH_END();
				/* No parent key */
				} else {
					toInsert = 1;
				}

				/* Add the key */
				if (toInsert) {
					/* Init child val */
					zval cval;

					/* Set child val */
					ZVAL_STR(&cval, key);

					/* Insert the child val in return array */
					ZEND_ASSERT(zend_hash_next_index_insert_new(ret, &cval) != NULL);
				}
			//}
		}
	} ZEND_HASH_FOREACH_END();

	/* Return value */
	return ret;
}
/* }}} */

/* {{{ static HashTable *bbcode_gen_parent(HashTable *ret, HashTable *tag, zend_string *cur TSRMLS_DC) { */
static HashTable *bbcode_gen_parent(HashTable *ret, HashTable *tag, zend_string *cur TSRMLS_DC) {
	/* Key string */
	zend_string *key;
	/* Value */
	zval *val;

	/* Iterate on each key val until hash end is reached */
	ZEND_HASH_FOREACH_STR_KEY_VAL(tag, key, val) {
		/* Check if key is not a string, value is null or not an array */
		if (key == NULL || val == NULL || Z_TYPE_P(val) != IS_ARRAY) {
			/* Return failure, useless to generate a hash when we will trigger an error later */
			return NULL;
		}

		/* Init type val */
		zval *type;

		/* Check that we have no type or that it's not single type */
		/* XXX: it's not possible to have in child the root tag */
		if ((type = zend_hash_str_find(Z_ARR_P(val), "type", strlen("type"))) == NULL || Z_LVAL_P(type) != BBCODE_TYPE_SINGLE) {
			/* Check that we are not on cur key */
			/* XXX: not really required, but it's stupid to allow same tag in child */
			//if (strcmp(ZSTR_VAL(cur), ZSTR_VAL(key)) != 0) {
				/* Init child val */
				zval *child;

				/* Init toInsert bool */
				unsigned char toInsert = 0;

				/* Check if we find child key */
				if ((child = zend_hash_str_find(Z_ARR_P(val), "child", strlen("child"))) != NULL) {
					/* Child value */
					zval *cval;

					/* Iterate on each val until hash end is reached */
					ZEND_HASH_FOREACH_VAL(Z_ARR_P(child), cval) {
						/* Check that child val is a string */
						if (Z_TYPE_P(cval) != IS_STRING) {
							return NULL;
						}

						/* Check that tkey is a string and is identical */
						if (strcmp(Z_STRVAL_P(cval), ZSTR_VAL(cur)) == 0) {
							toInsert = 1;
						}
					} ZEND_HASH_FOREACH_END();
				/* No child key */
				} else {
					toInsert = 1;
				}

				/* Add the key */
				if (toInsert) {
					/* Init parent val */
					zval pval;

					/* Set parent val */
					ZVAL_STR(&pval, key);

					/* Insert the parent val in return array */
					ZEND_ASSERT(zend_hash_next_index_insert_new(ret, &pval) != NULL);
				}
			//}
		}
	} ZEND_HASH_FOREACH_END();

	/* Return value */
	return ret;
}
/* }}} */

/* {{{ static zend_bool *bbcode_check_tag(HashTable *tag TSRMLS_DC) {
 * Check that tag hash is valid */
static zend_string *bbcode_check_tag(HashTable *tag TSRMLS_DC) {
	/* Key numeric value */
	zend_long idx;
	/* Key and root string */
	zend_string *key, *root = NULL;
	/* Value */
	zval *val;

	/* Key position */
	int pos = 0;

	/* Has a root */
	int hasRoot = 0;

	/* Iterate on each key val until hash end is reached */
	ZEND_HASH_FOREACH_KEY_VAL(tag, idx, key, val) {
		/* Check if key is not a string */
		if (key == NULL) {
			/* Display error about long key */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %ld[%d] from argument tag is not an expected string", idx, pos);
			/* Return failure */
			return NULL;
		/* Check that key do not contains a special character */
		} else if (strchr(ZSTR_VAL(key), '=') != NULL || strchr(ZSTR_VAL(key), '[') != NULL || strchr(ZSTR_VAL(key), ']') != NULL || strchr(ZSTR_VAL(key), '(') != NULL || strchr(ZSTR_VAL(key), ')') != NULL || strchr(ZSTR_VAL(key), '/') != NULL || strchr(ZSTR_VAL(key), '\\') != NULL) {
			/* Display error about key containing a special char */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d] from argument tag contains a special forbidden character: =[]()/\\", ZSTR_VAL(key), pos);
			/* Return failure */
			return NULL;
		}

		/* Check that value exists */
		if (val == NULL) {
			/* Display error about long key */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d] from argument tag is NULL instead of expected array", ZSTR_VAL(key), pos);
			/* Return failure */
			return NULL;
		/* Check that value is an array */
		} else if (Z_TYPE_P(val) != IS_ARRAY) {
			/* Display error about long key */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d] from argument tag is %s[%d] instead of expected array", ZSTR_VAL(key), pos, BBCODE_GET_TYPE(Z_TYPE_P(val)), Z_TYPE_P(val));
			/* Return failure */
			return NULL;
		}

		/* Store child tag array */
		HashTable *ctag = Z_ARR_P(val);
		/* Child key numeric value */
		zend_long cidx;
		/* Child key string */
		zend_string *ckey;
		/* Child value */
		zval *cval;

		/* Buffer */
		char *buf;

		/* Child key position */
		int cpos = 0, type, len;

		/* Detect if entry has a type, parent, child, open, close, default or arg key to display error if mandatory field is missing */
		unsigned char hasType = 0, hasParent = 0, hasChild = 0, hasOpen = 0, hasClose = 0, hasDefault = 0, hasArg = 0;

		/* Iterate on each ckey cval until hash end is reached */
		ZEND_HASH_FOREACH_KEY_VAL(ctag, cidx, ckey, cval) {
			/* Check if ckey is a string */
			if (!ckey) {
				/* Display error about long ckey */
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d]/%ld[%d] from argument tag is not an expected string", ZSTR_VAL(key), pos, cidx, cpos);
				/* Return failure */
				return NULL;
			}

			/* Check if ckey is empty */
			if (ZSTR_LEN(ckey) == 0) {
				/* Display error about long key */
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d]/%s[%d] from argument tag is empty not one of expected type|parent|child|open|close|default string", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos);
				/* Return failure */
				return NULL;
			/* Check if current ckey is type */
			} else if (strcmp("type", ZSTR_VAL(ckey)) == 0) {
				/* Extract type */
				type = Z_LVAL_P(cval);

				/* Check that current type is valid */
				if (
					type != BBCODE_TYPE_ROOT &&
					type != BBCODE_TYPE_MULTI &&
					type != BBCODE_TYPE_SINGLE
				) {
					/* Display error about unexpected type value */
					php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value '%d' for key %s[%d]/%s[%d] from argument tag is not one of expected BBCODE::TYPE_(ROOT|MULTI|SINGLE) constant", type, ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos);
					/* Return failure */
					return NULL;
				/* Check if root */
				} else if (type == BBCODE_TYPE_ROOT) {
					/* Grow hasRoot */
					hasRoot++;
				}

				/* Set hasType */
				hasType = 1;
			/* Check if current key is parent */
			} else if (strcmp("parent", ZSTR_VAL(ckey)) == 0) {
				/* Check that value exists */
				if (cval == NULL) {
					/* Display error about long key */
					php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d] from argument tag is NULL instead of expected array", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos);
					/* Return failure */
					return NULL;
				/* Check that value is an array */
				} else if (Z_TYPE_P(cval) != IS_ARRAY) {
					/* Display error about long key */
					php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d] from argument tag is %s[%d] instead of expected array", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos, BBCODE_GET_TYPE(Z_TYPE_P(cval)), Z_TYPE_P(cval));
					/* Return failure */
					return NULL;
				/* Check that value is an non empty array */
				} else if (zend_array_count(Z_ARR_P(cval)) == 0) {
					/* Display error about empty parent array */
					php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d] from argument tag is an empty array instead of expected filled array", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos);
					/* Return failure */
					return NULL;
				}

				/* Parent value */
				zval *pval;

				/* Child key position */
				int ppos = 0;

				/* Iterate on each val until hash end is reached */
				ZEND_HASH_FOREACH_VAL(Z_ARR_P(cval), pval) {
					/* Check that parent val is a string */
					/* XXX: pval == NULL case is catched here too */
					if (Z_TYPE_P(pval) != IS_STRING) {
						/* Display error about not string */
						php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d]/[%d] from argument tag is %s[%d] instead of expected string", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos, ppos, BBCODE_GET_TYPE(Z_TYPE_P(pval)), Z_TYPE_P(pval));
						/* Return failure */
						return NULL;
					}

					/* Tag key */
					zend_string *tkey;

					/* Reset found flag */
					unsigned char found = 0;

					/* Iterate on each key until hash end is reached */
					ZEND_HASH_FOREACH_STR_KEY(tag, tkey) {
						/* Check that tkey is a string and is identical */
						if (tkey != NULL && strcmp(Z_STRVAL_P(pval), ZSTR_VAL(tkey)) == 0) {
							/* We found parent value in tag keys*/
							found = 1;
						}
					} ZEND_HASH_FOREACH_END();

					/* Check if we found the key */
					if (!found) {
						/* Display error about long key */
						php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d]/%s[%d] from argument tag is not present in tag keys", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos, Z_STRVAL_P(pval), ppos);
						/* Return failure */
						return NULL;
					}

					/* Grow ppos */
					ppos++;
				} ZEND_HASH_FOREACH_END();

				/* Set hasParent */
				hasParent = 1;
			/* Check if current key is child */
			} else if (strcmp("child", ZSTR_VAL(ckey)) == 0) {
				/* Check that value exists */
				if (cval == NULL) {
					/* Display error about long key */
					php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d] from argument tag is NULL instead of expected array", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos);
					/* Return failure */
					return NULL;
				/* Check that value is an array */
				} else if (Z_TYPE_P(cval) != IS_ARRAY) {
					/* Display error about long key */
					php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d] from argument tag is %s[%d] instead of expected array", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos, BBCODE_GET_TYPE(Z_TYPE_P(cval)), Z_TYPE_P(cval));
					/* Return failure */
					return NULL;
				}

				/* Child value */
				zval *ccval;

				/* Child key position */
				int ccpos = 0;

				/* Iterate on each val until hash end is reached */
				ZEND_HASH_FOREACH_VAL(Z_ARR_P(cval), ccval) {
					/* Check that child val is a string */
					/* XXX: ccval == NULL case is catched here too */
					if (Z_TYPE_P(ccval) != IS_STRING) {
						/* Display error about not string */
						php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d]/%s[%d]/[%d] from argument tag is %s[%d] instead of expected string", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos, ccpos, BBCODE_GET_TYPE(Z_TYPE_P(ccval)), Z_TYPE_P(ccval));
						/* Return failure */
						return NULL;
					}

					/* Tag key */
					zend_string *tkey;

					/* Reset found flag */
					unsigned char found = 0;

					/* Iterate on each key until hash end is reached */
					ZEND_HASH_FOREACH_STR_KEY(tag, tkey) {
						/* Check that tkey is a string and is identical */
						if (tkey != NULL && strcmp(Z_STRVAL_P(ccval), ZSTR_VAL(tkey)) == 0) {
							/* We found child value in tag keys*/
							found = 1;
						}
					} ZEND_HASH_FOREACH_END();

					/* Check if we found the key */
					if (!found) {
						/* Display error about long key */
						php_error_docref(NULL TSRMLS_CC, E_WARNING, "Child value %s for key %s[%d]/%s[%d] is not present in tag keys", Z_STRVAL_P(ccval), ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos);
						/* Return failure */
						return NULL;
					}

					/* Grow ccpos */
					ccpos++;
				} ZEND_HASH_FOREACH_END();

				/* Set hasChild */
				hasChild = 1;
			/* Check if current key is open */
			} else if (strcmp("open", ZSTR_VAL(ckey)) == 0) {
				/* Set hasOpen */
				hasOpen = 1;
			/* Check if current key is close */
			} else if (strcmp("close", ZSTR_VAL(ckey)) == 0) {
				/* Set hasClose */
				hasClose = 1;
			/* Check if current key is default */
			} else if (strcmp("default", ZSTR_VAL(ckey)) == 0) {
				/* Set hasDefault */
				hasDefault = 1;
			/* Check if current key is arg */
			} else if (strcmp("arg", ZSTR_VAL(ckey)) == 0) {
				/* Set hasArg */
				hasArg = 1;
			/* Check if current key is unknown */
			} else {
				/* Display error about long key */
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d]/%s[%d] from argument tag is not one of expected type|parent|child|open|close|default string", ZSTR_VAL(key), pos, ZSTR_VAL(ckey), cpos);
				/* Return failure */
				return NULL;
			}

			/* Grow cpos */
			cpos++;
		} ZEND_HASH_FOREACH_END();

		/* Check if val array is immutable */
		/* XXX: this strange case happen with empty val array, the zend engine don't initialize a usable hashtable, to avoid triggering a segfault later when using the array, we fix it here */
		if ((Z_GC_FLAGS_P(val) & GC_IMMUTABLE) == GC_IMMUTABLE) {
			/* Allocate hash table */
			/* XXX: this is required to avoid segfault in zend_hash_add when the array is immutable */
			ALLOC_HASHTABLE(Z_ARR_P(val));

			/* Init hash table */
			zend_hash_init(Z_ARR_P(val), 0, NULL, ZVAL_PTR_DTOR, 0);

			/* Copy old values in new array */
			//XXX: disabled for now, array shoudln't need to be copied, immutable flag seems only present on empty arrays
			//zend_hash_copy(Z_ARR_P(val), ctag, (copy_ctor_func_t) zval_add_ref);
		}

		/* Check if entry has no type */
		/* TODO: make depend this code on a parse flag ? */
		if (!hasType) {
			/* Type key */
			zend_string *tkey = zend_string_init("type", strlen("type"), 0);

			/* Type value */
			zval tval;

			/* Check if key is '' */
			if (ZSTR_LEN(key) == 0) {
				/* Set tval value to root */
				ZVAL_LONG(&tval, (type = BBCODE_TYPE_ROOT));

				/* Grow hasRoot */
				hasRoot++;
			/* Check if key is img */
			} else if (strcmp("img", ZSTR_VAL(key)) == 0) {
				/* Set tval value to single */
				ZVAL_LONG(&tval, (type = BBCODE_TYPE_SINGLE));
			/* Handle other key as multi */
			} else {
				/* Set tval value to multi */
				ZVAL_LONG(&tval, (type = BBCODE_TYPE_MULTI));
			}

			/* Add new type key */
			ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), tkey, &tval) != NULL);

			/* Free type key */
			zend_string_release(tkey);

			/* Set hasType */
			hasType = 1;
		}

		/* Check for root type */
		if (type == BBCODE_TYPE_ROOT) {
			/* Set root */
			root = key;

			/* Check if has parent */
			if (hasParent) {
				/* Display error about parent entry */
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d] from argument tag of type BBCODE::TYPE_ROOT cannot have a parent entry", ZSTR_VAL(key), pos);
				/* Return failure */
				return NULL;
			}

			/* Check if has open entry */
			if (!hasOpen) {
				/* Key string */
				zend_string *mkey = zend_string_init("open", strlen("open"), 0);

				/* Value */
				zval mval;

				/* Check if tag is empty */
				if (ZSTR_LEN(key) == 0) {
					/* Init new value */
					ZVAL_EMPTY_STRING(&mval);
				/* Non empty tag */
				} else {
					/* Init buffer and length */
					buf = (char *)malloc((len = strlen("<>") + ZSTR_LEN(key) + (hasArg?strlen("%s"):0))*sizeof(char));

					/* Generate string in  */
					ZEND_ASSERT(sprintf(buf, "<%s%s>", ZSTR_VAL(key), hasArg?"%s":"") == len);

					/* Init new value */
					ZVAL_STRINGL(&mval, buf, len);

					/* Free buffer */
					free(buf);
				}

				/* Add new key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), mkey, &mval) != NULL);

				/* Free key */
				zend_string_release(mkey);

				/* Set hasOpen */
				hasOpen = 1;
			}

			/* Check if has close entry */
			if (!hasClose) {
				/* Key string */
				zend_string *mkey = zend_string_init("close", strlen("close"), 0);

				/* Value */
				zval mval;

				/* Check if tag is empty */
				if (ZSTR_LEN(key) == 0) {
					/* Init new value */
					ZVAL_EMPTY_STRING(&mval);
				/* Non empty tag */
				} else {
					/* Init buffer and length */
					buf = (char *)malloc((len = strlen("</>") + ZSTR_LEN(key))*sizeof(char));

					/* Generate string in  */
					ZEND_ASSERT(sprintf(buf, "</%s>", ZSTR_VAL(key)) == len);

					/* Init new value */
					ZVAL_STRINGL(&mval, buf, len);

					/* Free buffer */
					free(buf);
				}

				/* Add new key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), mkey, &mval) != NULL);

				/* Free key */
				zend_string_release(mkey);

				/* Set hasClose */
				hasClose = 1;
			}

			/* Check if has not default */
			if (!hasDefault) {
				/* Key string */
				zend_string *mkey = zend_string_init("default", strlen("default"), 0);

				/* Value */
				zval mval;

				/* Set value */
				ZVAL_STRING(&mval, "%s");

				/* Add new key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), mkey, &mval) != NULL);

				/* Free key */
				zend_string_release(mkey);

				/* Set hasDefault */
				hasDefault = 1;
			}

			/* Compute child here */
			if (!hasChild) {
				/* Child value */
				zval cval;

				/* Init new child val array */
				ZVAL_NEW_ARR(&cval);

				/* Init hash table */
				zend_hash_init(Z_ARR(cval), 0, NULL, ZVAL_PTR_DTOR, 0);

				/* Compute and set child here */
				/* XXX: we don't check or trigger error inside this function as these tests are done in current loop */
				if (bbcode_gen_child(Z_ARR(cval), tag, key) != NULL) {
					/* Child key string */
					zend_string *ckey = zend_string_init("child", strlen("child"), 0);

					/* Add new child key */
					ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), ckey, &cval) != NULL);

					/* Free type key */
					zend_string_release(ckey);

					/* Set hasChild */
					hasChild = 1;
				/* Child generation reached an error case */
				/* XXX: it will trigger error later */
				} else {
					/* Release hash table */
					zend_array_destroy(Z_ARR(cval));
				}
			}
		/* Check for multi type */
		} else if (type == BBCODE_TYPE_MULTI) {
			/* Check if has open entry */
			if (!hasOpen) {
				/* Key string */
				zend_string *mkey = zend_string_init("open", strlen("open"), 0);

				/* Value */
				zval mval;

				/* Check if tag is empty */
				if (ZSTR_LEN(key) == 0) {
					/* Init new value */
					ZVAL_EMPTY_STRING(&mval);
				/* Non empty tag */
				} else {
					/* Init buffer and length */
					buf = (char *)malloc((len = strlen("<>") + ZSTR_LEN(key) + (hasArg?strlen("%s"):0))*sizeof(char));

					/* Generate string in  */
					ZEND_ASSERT(sprintf(buf, "<%s%s>", ZSTR_VAL(key), hasArg?"%s":"") == len);

					/* Init new value */
					ZVAL_STRINGL(&mval, buf, len);

					/* Free buffer */
					free(buf);
				}

				/* Add new key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), mkey, &mval) != NULL);

				/* Free key */
				zend_string_release(mkey);

				/* Set hasOpen */
				hasOpen = 1;
			}

			/* Check if has close entry */
			if (!hasClose) {
				/* Key string */
				zend_string *mkey = zend_string_init("close", strlen("close"), 0);

				/* Value */
				zval mval;

				/* Check if tag is empty */
				if (ZSTR_LEN(key) == 0) {
					/* Init new value */
					ZVAL_EMPTY_STRING(&mval);
				/* Non empty tag */
				} else {
					/* Init buffer and length */
					buf = (char *)malloc((len = strlen("</>") + ZSTR_LEN(key))*sizeof(char));

					/* Generate string in  */
					ZEND_ASSERT(sprintf(buf, "</%s>", ZSTR_VAL(key)) == len);

					/* Init new value */
					ZVAL_STRINGL(&mval, buf, len);

					/* Free buffer */
					free(buf);
				}

				/* Add new key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), mkey, &mval) != NULL);

				/* Free key */
				zend_string_release(mkey);

				/* Set hasClose */
				hasClose = 1;
			}

			/* Check if has not default */
			if (!hasDefault) {
				/* Key string */
				zend_string *mkey = zend_string_init("default", strlen("default"), 0);

				/* Value */
				zval mval;

				/* Set value */
				ZVAL_STRING(&mval, "%s");

				/* Add new key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), mkey, &mval) != NULL);

				/* Free key */
				zend_string_release(mkey);

				/* Set hasDefault */
				hasDefault = 1;
			}

			/* Compute parent here */
			if (!hasParent) {
				/* Parent key string */
				zend_string *pkey = zend_string_init("parent", strlen("parent"), 0);

				/* Parent value */
				zval pval;

				/* Init new parent val array */
				ZVAL_NEW_ARR(&pval);

				/* Init hash table */
				zend_hash_init(Z_ARR(pval), 0, NULL, ZVAL_PTR_DTOR, 0);

				/* Compute and set parent here */
				/* XXX: we don't check or trigger error inside this function as these tests are done in current loop */
				/* TODO: change this assert to a if() and free pval on error, error will trigger in later loop (code coverage tests to do) */
				ZEND_ASSERT(bbcode_gen_parent(Z_ARR(pval), tag, key) != NULL);

				/* Add new parent key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), pkey, &pval) != NULL);

				/* Free type key */
				zend_string_release(pkey);

				/* Set hasParent */
				hasParent = 1;
			}

			/* Compute child here */
			if (!hasChild) {
				/* Child value */
				zval cval;

				/* Init new child val array */
				ZVAL_NEW_ARR(&cval);

				/* Init hash table */
				zend_hash_init(Z_ARR(cval), 0, NULL, ZVAL_PTR_DTOR, 0);

				/* Compute and set child here */
				/* XXX: we don't check or trigger error inside this function as these tests are done in current loop */
				if (bbcode_gen_child(Z_ARR(cval), tag, key) != NULL) {
					/* Child key string */
					zend_string *ckey = zend_string_init("child", strlen("child"), 0);

					/* Add new child key */
					ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), ckey, &cval) != NULL);

					/* Free type key */
					zend_string_release(ckey);

					/* Set hasChild */
					hasChild = 1;
				/* Child generation reached an error case */
				/* XXX: it will trigger error later */
				} else {
					/* Release hash table */
					zend_array_destroy(Z_ARR(cval));
				}
			}
		/* Check for single type */
		} else if (type == BBCODE_TYPE_SINGLE) {
			/* Check if has open entry */
			if (hasOpen) {
				/* Display error about open entry */
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d] from argument tag of type BBCODE::TYPE_SINGLE cannot have an open entry", ZSTR_VAL(key), pos);
				/* Return failure */
				return NULL;
			}

			/* Check if has close entry */
			if (hasClose) {
				/* Display error about close entry */
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d] from argument tag of type BBCODE::TYPE_SINGLE cannot have a close entry", ZSTR_VAL(key), pos);
				/* Return failure */
				return NULL;
			}

			/* Check if has not default */
			if (!hasDefault) {
				/* Key string */
				zend_string *skey = zend_string_init("default", strlen("default"), 0);

				/* Value */
				zval sval;

				/* Check if key is img */
				if (strcmp("img", ZSTR_VAL(key)) == 0) {
					/* Init buffer and length */
					buf = (char *)malloc((len = strlen("<img src=\"%s\"/>") + (hasArg?strlen("%s"):0))*sizeof(char));

					/* Generate string in  */
					ZEND_ASSERT(sprintf(buf, "<img src=\"%%s\"%s/>", hasArg?"%s":"") == len);
				} else {
					/* Init buffer and length */
					buf = (char *)malloc((len = strlen("</>") + ZSTR_LEN(key) + (hasArg?strlen("%s"):0))*sizeof(char));

					/* Generate string in  */
					ZEND_ASSERT(sprintf(buf, "<%s%s/>", ZSTR_VAL(key), hasArg?"%s":"") == len);
				}

				/* Init new value */
				ZVAL_STRINGL(&sval, buf, len);

				/* Free buffer */
				free(buf);

				/* Add new key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), skey, &sval) != NULL);

				/* Free key */
				zend_string_release(skey);

				/* Set hasDefault */
				hasDefault = 1;
			}

			/* Check if has child */
			if (hasChild) {
				/* Display error about child entry */
				php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %s[%d] from argument tag of type BBCODE::TYPE_SINGLE cannot have a child entry", ZSTR_VAL(key), pos);
				/* Return failure */
				return NULL;
			}

			/* Compute parent here */
			if (!hasParent) {
				/* Parent key string */
				zend_string *pkey = zend_string_init("parent", strlen("parent"), 0);

				/* Parent value */
				zval pval;

				/* Init new parent val array */
				ZVAL_NEW_ARR(&pval);

				/* Init hash table */
				zend_hash_init(Z_ARR(pval), 0, NULL, ZVAL_PTR_DTOR, 0);

				/* Compute and set parent here */
				/* XXX: we don't check or trigger error inside this function as these tests are done in current loop */
				/* TODO: change this asser to a if() and free pval on error, error will trigger in later loop (code coverage tests to do) */
				ZEND_ASSERT(bbcode_gen_parent(Z_ARR(pval), tag, key) != NULL);

				/* Add new parent key */
				ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), pkey, &pval) != NULL);

				/* Free type key */
				zend_string_release(pkey);

				/* Set hasParent */
				hasParent = 1;
			}
		}

		/* Generate open and close pattern for tag with child */
		if (hasChild) {
			/* Child array and value */
			zval *child, *ccval;

			/* Close pattern length */
			int len2, cpos2;

			/* Close pattern buf */
			char *buf2;

			/* Retrieve child array */
			/* XXX: child should exists here and be an array */
			ZEND_ASSERT(((child = zend_hash_str_find(Z_ARR_P(val), "child", strlen("child"))) != NULL) && Z_TYPE_P(child) == IS_ARRAY);

			/* Init open pattern buffer and length */
			/* XXX: lagest bbcode tag length seems to be 7, we guess that 10 is enough to contain every tag and pipe */
			buf = (char *)malloc((len = strlen("/\\[()(?:=([^\\]]*))?\\]/") + zend_array_count(Z_ARR_P(child))*10)*sizeof(char));

			/* Init close pattern buffer and length */
			/* XXX: lagest bbcode tag length seems to be 7, we guess that 10 is enough to contain every tag and pipe */
			buf2 = (char *)malloc((len2 = strlen("/\\[\\/()\\]/") + zend_array_count(Z_ARR_P(child))*10)*sizeof(char));

			/* Init cpos and cpos2 */
			cpos = 4; cpos2 = 6;

			/* Assign open pattern begin */
			ZEND_ASSERT(memcpy(buf, &"/\\[(", 4) != NULL);

			/* Assign close pattern begin */
			ZEND_ASSERT(memcpy(buf2, &"/\\[\\/(", 6) != NULL);

			/* Iterate on each val until hash end is reached */
			ZEND_HASH_FOREACH_VAL(Z_ARR_P(child), ccval) {
				/* Check that child val is a string */
				/* XXX: ccval should be an array here */
				ZEND_ASSERT(Z_TYPE_P(ccval) == IS_STRING);

				/* Init zval node and type */
				zval *znode, *ztype;

				/* Init zend_long type */
				zend_long ltype;

				/* Retrieve the node */
				ZEND_ASSERT((znode = zend_hash_find(tag, Z_STR_P(ccval))) != NULL && Z_TYPE_P(znode) == IS_ARRAY);

				/* Try to retrieve the type */
				//ZEND_ASSERT((ztype = zend_hash_str_find(Z_ARR_P(znode), "type", strlen("type"))) != NULL && Z_TYPE_P(ztype) == IS_LONG);
				/* XXX: the type may not yet available here, it is fixed when checking the tag itself later */
				if ((ztype = zend_hash_str_find(Z_ARR_P(znode), "type", strlen("type"))) == NULL || Z_TYPE_P(ztype) != IS_LONG) {
					/* Check if empty tag */
					if (Z_STRLEN_P(ccval) == 0) {
						/* Set as root */
						ltype = BBCODE_TYPE_ROOT;
					} else if (strcmp("img", Z_STRVAL_P(ccval)) == 0) {
						/* Set as single */
						ltype = BBCODE_TYPE_SINGLE;
					} else {
						/* Set as multi */
						ltype = BBCODE_TYPE_MULTI;
					}
				/* Type zval available and usable */
				} else {
					/* Set type from zval value */
					ltype = Z_LVAL_P(ztype);
				}

				/* Check if not first string to add */
				if (cpos > 4) {
					/* Set pipe before next tag */
					*(buf+cpos) = '|';
					/* Move position */
					cpos++;
					/* Check if not single type */
					if (ltype != BBCODE_TYPE_SINGLE) {
						/* Set pipe before next tag */
						*(buf2+cpos2) = '|';
						/* Move position */
						cpos2++;
					}
				}

				/* Check if not single type */
				if (ltype != BBCODE_TYPE_SINGLE) {
					/* Grow buf if necessary */
					if (cpos + Z_STRLEN_P(ccval) + 18 > len) {
						/* We resize buf to current length(cpos), the string size plus eighteen char for trailing pattern */
						buf = (char *)realloc(buf, (len = cpos + Z_STRLEN_P(ccval) + 18)*sizeof(char));
					}

					/* Copy string in buf */
					ZEND_ASSERT(memcpy(buf+cpos, Z_STRVAL_P(ccval), Z_STRLEN_P(ccval)) != NULL);

					/* Move cpos */
					cpos += Z_STRLEN_P(ccval);

					/* Grow buf2 if necessary */
					if (cpos2 + Z_STRLEN_P(ccval) + 4 > len2) {
						/* We resize buf2 to current length(cpos2), the string size plus four char for trailing pattern */
						buf2 = (char *)realloc(buf2, (len2 = cpos2 + Z_STRLEN_P(ccval) + 4)*sizeof(char));
					}

					/* Copy string in buf2 */
					ZEND_ASSERT(memcpy(buf2+cpos2, Z_STRVAL_P(ccval), Z_STRLEN_P(ccval)) != NULL);

					/* Move cpos2 */
					cpos2 += Z_STRLEN_P(ccval);
				/* Single type case */
				} else {
					/* Grow buf if necessary */
					if (cpos + Z_STRLEN_P(ccval) + strlen("(?:\\/)?") + 18 > len) {
						/* We resize buf to current length(cpos), the string size plus three plus eighteen char for trailing pattern */
						buf = (char *)realloc(buf, (len = cpos + Z_STRLEN_P(ccval) + strlen("(?:\\/)?") + 18)*sizeof(char));
					}

					/* Copy string in buf */
					ZEND_ASSERT(memcpy(buf+cpos, Z_STRVAL_P(ccval), Z_STRLEN_P(ccval)) != NULL);

					/* Append pattern in buf */
					ZEND_ASSERT(memcpy(buf+cpos+Z_STRLEN_P(ccval), &"(?:\\/)?", strlen("(?:\\/)?")) != NULL);

					/* Move cpos */
					cpos += Z_STRLEN_P(ccval) + strlen("(?:\\/)?");
				}
			} ZEND_HASH_FOREACH_END();

			/* Assign open pattern begin */
			ZEND_ASSERT(memcpy(buf+cpos, &")(?:=([^\\]]*))?\\]/", 18) != NULL);

			/* Assign open pattern begin */
			ZEND_ASSERT(memcpy(buf2+cpos2, &")\\]/", 4) != NULL);

			/* Open key string */
			zend_string *okey = zend_string_init("opattern", strlen("opattern"), 0);

			/* Close key string */
			zend_string *ckey = zend_string_init("cpattern", strlen("cpattern"), 0);

			/* Open and close value */
			zval oval, cval;

			/* Set open value */
			ZVAL_STRINGL(&oval, buf, cpos + 18);

			/* Set close value */
			ZVAL_STRINGL(&cval, buf2, cpos2 + 4);

			/* Free buffers */
			free(buf); free(buf2);

			/* Add open key */
			ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), okey, &oval) != NULL);

			/* Add close key */
			ZEND_ASSERT(zend_hash_add(Z_ARR_P(val), ckey, &cval) != NULL);

			/* Free open key */
			zend_string_release(okey);

			/* Free close key */
			zend_string_release(ckey);
		}

		/* Grow pos */
		pos++;
	} ZEND_HASH_FOREACH_END();

	/* Check if not one unique root */
	if (hasRoot != 1) {
			/* Display error about multiple root */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Tag array has %d BBCODE::TYPE_ROOT entry instead of expected unique one", hasRoot);
			/* Return failure */
			return NULL;
	}

	/* Duplicate root zend string to avoid it beeing freed */
	return zend_string_copy(root);

#if 0

	/* Return success */
	return root;
#endif
}
/* }}} */

/* {{{ static zend_bool *bbcode_check_smiley(HashTable *smiley TSRMLS_DC) {
 * Check that smiley hash is valid */
static zend_bool bbcode_check_smiley(HashTable *smiley TSRMLS_DC) {
	/* Key numeric value */
	zend_long idx;
	/* Key string */
	zend_string *key;
	/* Value */
	zval *val;

	/* Key position */
	int pos = 0;

	/* Iterate on each key val until hash end is reached */
	ZEND_HASH_FOREACH_KEY_VAL(smiley, idx, key, val) {
		/* Check that key is a string */
		if (key == NULL) {
			/* Display error about long key */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Key %ld[%d] from argument smiley is not an expected string", idx, pos);
			/* Return failure */
			return 0;
		}

		/* Check that value exists */
		if (val == NULL) {
			/* Display error about long key */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d] from argument smiley is NULL instead of expected array", ZSTR_VAL(key), pos);
			/* Return failure */
			return 0;
		/* Check that value is not an array */
		} else if (Z_TYPE_P(val) == IS_ARRAY) {
			/* Display error about long key */
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Value for key %s[%d] from argument smiley is an array instead of expected string", ZSTR_VAL(key), pos);
			/* Return failure */
			return 0;
		}

		/* Grow pos */
		pos++;
	} ZEND_HASH_FOREACH_END();

	/* Return success */
	return 1;
}
/* }}} */

/* {{{ PHP_METHOD(BBCode, __construct) { */
PHP_METHOD(BBCode, __construct) {
	/* Init tag hash */
	HashTable *tag = NULL;

	/* Init smiley hash */
	HashTable *smiley = NULL;

	/* Init flag long */
	zend_long flag = BBCODE_DEFAULT_FLAG;

	/* Zend error handling */
	zend_error_handling error_handling;

	/* TODO: set tag array to a default value like this in case of missing/empty tag array :
	 * [ '' => [ ? ] ]
	 * (maybe later)
	 */

	/* TODO: init smiley with default list ? what about img storage ?
	 * TODO: use prefix in an ini parameter ? ini_setable ?
	 * TODO: make sure it's not really a global and per-app param ?
	 * (maybe later)
	 */

	zend_replace_error_handling(EH_THROW, bbcode_exception_ce, &error_handling);
	/* Parse parameters */
	/*if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "h|hl", &tag, &smiley, &flag) == FAILURE) { return; }*/
	ZEND_PARSE_PARAMETERS_START(1, 3)
		/* Tag arg is required */
		Z_PARAM_ARRAY_HT(tag)
		/* We switch to optional args */
		Z_PARAM_OPTIONAL
		/* Smiley arg is optional and may be null */
		Z_PARAM_ARRAY_HT_EX(smiley, 1, 0)
		/* Flag arg is optional and may be null */
		Z_PARAM_LONG(flag)
	/* End parameters parse */
	ZEND_PARSE_PARAMETERS_END();

	/* Retrieve the pointer to this object */
	bbcode_object *obj = Z_BBCODE_P(getThis());

	/* Verify that the pointer is leading somewhere */
	assert(obj != NULL);

	/* Set flag value */
	obj->flag = flag;

	/* Duplicate hash to avoid modifications on original argument */
	obj->tag = zend_array_dup(tag);

	/* Save tag argument hash in current object */
	if ((obj->root = bbcode_check_tag(obj->tag)) == NULL) {
		/* Restore error handler */
		zend_restore_error_handling(&error_handling);

		/* Release the tag hash */
		zend_array_destroy(obj->tag);

		/* Free the zend object */
		//XXX: seems useless, it don't seems possible to null the contructor return value...
		ZVAL_NULL(getThis() TSRMLS_CC);

		/* Free the object */
		efree(obj);

		/* Stop here */
		return;
	}

	/* Check if smiley argument is available */
	if (smiley) {
		/* Duplicate hash to avoid modifications on original argument */
		obj->smiley = zend_array_dup(smiley);

		/* Save smiley argument hash in current object */
		if (!bbcode_check_smiley(obj->smiley)) {
			/* Restore error handler */
			zend_restore_error_handling(&error_handling);

			/* Release the root string */
			zend_string_release(obj->root);

			/* Release the tag hash */
			zend_array_destroy(obj->tag);

			/* Release the smiley hash */
			zend_array_destroy(obj->smiley);

			/* Free the object */
			//XXX: seems useless, it don't seems possible to null the contructor return value...
			ZVAL_NULL(getThis() TSRMLS_CC);

			/* Free the object */
			efree(obj);

			/* Stop here */
			return;
		}
	}

	/* Restore error handler */
	zend_restore_error_handling(&error_handling);
}
/* }}} */

/* {{{ PHP_METHOD(BBCode, __destruct) {
 * XXX: this function is called by bbcode_destroy function
 */
PHP_METHOD(BBCode, __destruct) {
//XXX: disable this code, we don't need to retrieve obj as we do nothing with userland destructor for now
#if 0
	/* Retrieve the pointer to this object */
	bbcode_object *obj = Z_BBCODE_P(getThis());

	/* Verify that the pointer is leading somewhere */
	assert(obj != NULL);
#endif
}
/* }}} */

/*  */
//[HashTable *ret][zend_string *current][char *str][int offset][int length]
static char *bbcode_parse(HashTable *tag, zend_string *cur, char *rs, int *ros, int *rlen, char *str, int len TSRMLS_DC) {
	/* Check if return string is not initialized or shorter than wanted length */
	if (rs == NULL || *rlen - *ros <= 1024) {
		/* Compute new length */
		*rlen = (rs == NULL) ? 2048 : *rlen + 1024;

		/* Grow the return string */
		ZEND_ASSERT((rs = (char *)realloc(rs, (*rlen)*sizeof(char))) != NULL);
	}

	//DEBUG: for reminder
	//str=skip0[r]string0[a=arg0]string1[/a]string2[b]string3[/b][/r]skip1
	//len=64

	/* Init zval for current node and type */
	zval *znode, *ztype;

	/* Retrieve current tag hash as zval */
	ZEND_ASSERT((znode = zend_hash_find(tag, cur)) != NULL && Z_TYPE_P(znode) == IS_ARRAY);

	/* Retrieve current tag type */
	ZEND_ASSERT((ztype = zend_hash_str_find(Z_ARR_P(znode), "type", strlen("type"))) != NULL && Z_TYPE_P(ztype) == IS_LONG);

	/* Retrieve current tag open */
	/* XXX: may be NULL if not present */
	zval *zopen = zend_hash_str_find(Z_ARR_P(znode), "open", strlen("open"));

	/* Retrieve current tag close */
	/* XXX: may be NULL if not present */
	zval *zclose = zend_hash_str_find(Z_ARR_P(znode), "close", strlen("close"));

	/* Retrieve current tag default */
	/* XXX: may be NULL if not present */
	zval *zdefault = zend_hash_str_find(Z_ARR_P(znode), "default", strlen("default"));

	/* Retrieve current tag arg */
	/* XXX: may be NULL if not present */
	zval *zarg = zend_hash_str_find(Z_ARR_P(znode), "arg", strlen("arg"));

	/* The arg zend string */
	zend_string *argstr = NULL;

	/* Check if we have a not root and not empty tag */
	if (Z_LVAL_P(ztype) != BBCODE_TYPE_ROOT && strcmp(ZSTR_VAL(cur), "") != 0) {
		/* Regular expression */
		zend_string *regex;

		/* Open and close pattern */
		char *openstr, *closestr, *found;

		/* Open and close pattern length */
		int openlen, closelen;

		/* Alloc open pattern buffer and set length */
		openstr = (char *)malloc(((openlen = strlen("/\\[\\]/") + (zarg == NULL ? strlen("(?:=[^\\]]*)?") : strlen("(?:=([^\\]]*))?")) + ZSTR_LEN(cur)) + 1)*sizeof(char));

		/* Alloc close pattern buffer and set length */
		closestr = (char *)malloc(((closelen = strlen("[/]") + ZSTR_LEN(cur)) + 1)*sizeof(char));

		/* Create open tag string */
		ZEND_ASSERT(snprintf(openstr, openlen + 1, "/\\[%s%s\\]/", ZSTR_VAL(cur), (zarg == NULL ? "(?:=[^\\]]*)?" : "(?:=([^\\]]*))?")) == openlen);

		/* Duplicate open tag in regex */
		regex = zend_string_init(openstr, openlen, 0);

		/* Free the close string */
		free(closestr);

		/* Compiled regular expression */
		pcre_cache_entry *pce;

		/* Array for subpatterns and return value */
		zval rv, subpats;

		/* Init subpats array */
		/* XXX: if we don't initialize the array first, some strange memory corruption happen inside php_pcre_match_impl call */
		array_init(&subpats);

		/* Compile regex or get it from cache. */
		/* TODO: see if we handle the error here ? (maybe later)
		 * XXX: it seems unlikely any error could be recoverable if pcre function throw something
		 * XXX: special char in tag is already protected earlier
		 * XXX: maybe just check that the pcre error trigger an exception or something uncatchable in all case
		 * XXX: see function pcre_get_compiled_regex_cache in ext/pcre/php_pcre.c +530 */
		ZEND_ASSERT((pce = pcre_get_compiled_regex_cache(regex)) != NULL);

		/* Increase pce refcount */
		/* XXX: we use this instead of pce->refcount++; to avoid dereferencing compile error  */
		php_pcre_pce_incref(pce);

		/* Call zend pcre implementation */
		php_pcre_match_impl(pce, str, len, &rv, &subpats, 0, 0, 0, 0);

		/* Decrease pce refcount */
		/* XXX: we use this instead of pce->refcount--; to avoid dereferencing compile error  */
		php_pcre_pce_decref(pce);

		/* Check that we found an occurence */
		if (Z_TYPE(rv) == IS_LONG && Z_LVAL(rv) == 1) {
			/* Init match zval */
			zval *match;

			/* Init the matched tag string */
			//char *tstr;

			/* Test that subpats is an array */
			ZEND_ASSERT(Z_TYPE(subpats) == IS_ARRAY);

			/* Test that subpats members count is right */
			ZEND_ASSERT(zend_array_count(Z_ARR(subpats)) == (zarg == NULL ? 1 : 2));

			/* Retrieve the matched full tag string */
			ZEND_ASSERT((match = zend_hash_index_find(Z_ARR(subpats), 0)) != NULL);

			/* Test that match is a string */
			ZEND_ASSERT(Z_TYPE_P(match) == IS_STRING);

			/* Find the tag in string */
			if ((found = (char *)zend_memnstr(str, Z_STRVAL_P(match), Z_STRLEN_P(match), str+len))) {
				/* Move str to found position + matched tag string */
				str = found + Z_STRLEN_P(match);
			}

			/* Check if have and arg to retrieve */
			if (zarg != NULL) {
				/* Retrieve the matched arg string */
				ZEND_ASSERT((match = zend_hash_index_find(Z_ARR(subpats), 1)) != NULL);

				/* Test that match is a string */
				ZEND_ASSERT(Z_TYPE_P(match) == IS_STRING);

				/* Save arg string */
				argstr = Z_STR_P(match);
			}

			//XXX: code unfinished, these todo are normal, work start here :)
			//TODO: set the new search position to the current offset plus found subpats[0] string length
			//TODO: save argument string for appending in return string if zarg is not NULL
			//TODO: see what happen if we find start tag and not end
			//TODO: see what happen if we find no start tag and no end
			//TODO: see what happen if we find no start tag and no end

		/* No occurence found */
		} else {
			/* XXX: nothing to do ? skip end tag search ??? return to parent level ? */
		}

		/* Destroy subpats array */
		zend_array_destroy(Z_ARR(subpats));

		/* Create close tag string */
		//ZEND_ASSERT(snprintf(closestr, closelen + 1, "/\\[\\/%s\\]/", ZSTR_VAL(cur)) == closelen);
		ZEND_ASSERT(snprintf(closestr, closelen + 1, "[/%s]", ZSTR_VAL(cur)) == closelen);

		/* Look for close tag */
		/* XXX: watch out, this is a maximum length to consider, the close tag may be present earlier for current leaf */
		if ((found = (char *)zend_memnrstr(str, closestr, closelen, str+len))) {
			/* Reduce len by the size of trailing string since close tag */
			len -= len + str - found;
		}

		/* Free openstr and closestr */
		free(openstr);
	}

	printf("str=%.*s\n", len, str);
	fflush(NULL);

	return NULL;

#if 0
	fflush(NULL);
	printf("str=%s\n", str);
#endif

	/* XXX: debug */
	zval ztag;
	ZVAL_ARR(&ztag, tag);
	php_debug_zval_dump(&ztag, 0);
	fflush(NULL);
	exit(1);

	php_debug_zval_dump(zopen, 0);
	php_debug_zval_dump(zclose, 0);
	php_debug_zval_dump(zdefault, 0);
	fflush(NULL);
	exit(1);

	if (argstr != NULL) {
		printf("arg=%s[%ld]\n", ZSTR_VAL(argstr), ZSTR_LEN(argstr));
		fflush(NULL);
		exit(1);
	}

	//XXX: same here unfinished, code start here :)
	//TODO: check what happen when we have something like that : [t]s[/t][t]p[/t]
	//1: compute open and close tag
	//2: extract end of open tag position and begin of close tag reverse searched in current space
	//(if we don't find end tag, consider positionning to end of string or rise error ? parse flag involved here)
	//3: append to return string open tag
	//4: loop for child tags in subspace and trigger recursion on space between current open and close tags
	//(loop involved here)
	//5: duplicate plain strings between each child in return string
	//6: append the close tag

#if 0
	/* Check if non empty string (other tag that empty root) */
	if (strcmp(ZSTR_VAL(cur), "") != 0) {
		//TODO: generate search patterns for opening tag and closing tag
		//TODO: search position of opening tag
		//TODO: search close tag
		//TODO: search for child tag
	/* Non root tag */
	} else {
		//Nothing to do, search offset for child will be 0 and length of search to len
		//TODO: search current end tag here ?
	}

	/* Search next child node */

	/**/
	//bbcode_search_node(ret,
#endif

	/* Return string by default */
	return rs;
}

/* { { { PHP_METHOD(BBCode, parse) {
 */
PHP_METHOD(BBCode, parse) {
	/* Init string to parse */
	zend_string *str = NULL;

	/* Return string */
	//TODO: voir si on crée pas une struct pour éviter de trainer 3 arguments
	char *rs = NULL;

	/* Return offset and length */
	int ros = 0, rlen = 0;

	/* Parse parameters */
	/*if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "S", &str) == FAILURE) { return; }*/
	ZEND_PARSE_PARAMETERS_START(1, 1)
		Z_PARAM_STR(str)
	ZEND_PARSE_PARAMETERS_END();

	/* Retrieve the pointer to this object */
	bbcode_object *obj = Z_BBCODE_P(getThis());

	/* Init return string and set length to string * 2 plus last null char */
	//ZEND_ASSERT((rs = (char *)malloc((rlen = (2*Z_STRLEN(str)) + 1)*sizeof(char)) != NULL);

	/* Go compute the tree */
	bbcode_parse(obj->tag, obj->root, rs, &ros, &rlen, ZSTR_VAL(str), ZSTR_LEN(str));

	/* Free return string */
	free(rs);

#if 0
	/* Retrieve root node */
	zval *root = zend_hash_find(obj->tag, obj->root);

	bbcode_

				/* Retrieve child array */
				/* XXX: child should exists here and be an array */
				ZEND_ASSERT(((child = zend_hash_str_find(Z_ARR_P(val), "child", strlen("child"))) != NULL) && Z_TYPE_P(child) == IS_ARRAY);
#endif

#if BBCODE_DEBUG
	/* Tag */
	zval tag;

	/* Init new child val array */
	ZVAL_ARR(&tag, obj->tag);

	php_debug_zval_dump(&tag, 0);
	fflush(NULL);
#endif

	//printf("flag=%ld\n", obj->flag);
	//printf("die ici: %s +%d\n", __FILE__, __LINE__);

	//TODO: create a tree for storing result
	//TODO: apply recursively search of next possible tags (or a closing parent tag ?) regexp and store result inside
	//TODO: d
	//TODO: apply the smiley parsing on string

	/* TODO: the return string */
	RETURN_STR(str);
}
/* }}} */

/* {{{ bbcode_methods[]
 *
 * Every BBCode visible function must have an entry in bbcode_methods[].
 */
const zend_function_entry bbcode_methods[] = {
	PHP_ME(BBCode, __construct, bbcode_construct_arginfo, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
	PHP_ME(BBCode, __destruct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_DTOR)
	PHP_ME(BBCode, parse, bbcode_parse_arginfo, ZEND_ACC_PUBLIC)
	PHP_FE_END
};
/* }}} */

/* {{{ PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(bbcode) {
	/* If you have INI entries, uncomment these lines
	REGISTER_INI_ENTRIES();
	*/

	/* Tmp zend class entry*/
	zend_class_entry tmp_ce;

	/* Init BBCode class entry */
	INIT_CLASS_ENTRY(tmp_ce, "BBCode", bbcode_methods);

	/* Register BBCode class entry */
	bbcode_ce = zend_register_internal_class(&tmp_ce TSRMLS_CC);
	bbcode_ce->create_object = bbcode_create;

	/* Generate const */
	BBCODE_DEF(BBCODE_GEN_CONST)

	/* Init BBCodeException class entry */
	INIT_CLASS_ENTRY(tmp_ce, "BBCodeException", NULL);

	/* Register BBCodeException class entry */
	bbcode_exception_ce = zend_register_internal_class_ex(&tmp_ce, zend_exception_get_default(TSRMLS_C) TSRMLS_CC);

	/* Send success */
	return SUCCESS;
}
/* }}} */

/* {{{ PHP_MSHUTDOWN_FUNCTION
 */
PHP_MSHUTDOWN_FUNCTION(bbcode) {
	/* uncomment this line if you have INI entries
	UNREGISTER_INI_ENTRIES();
	*/
	return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
 */
PHP_MINFO_FUNCTION(bbcode) {
	php_info_print_table_start();
	php_info_print_table_header(2, PHP_BBCODE_NAME " support", "enabled");
	php_info_print_table_row(2, "Extension version", PHP_BBCODE_VERSION);
	php_info_print_table_end();

	/* Remove comments if you have entries in php.ini
	DISPLAY_INI_ENTRIES();
	*/
}
/* }}} */

/* {{{ bbcode_module_entry
 */
zend_module_entry bbcode_module_entry = {
	STANDARD_MODULE_HEADER,
	PHP_BBCODE_NAME,			/* extension name */
	NULL,						/* function list */
	PHP_MINIT(bbcode),			/* process startup */
	PHP_MSHUTDOWN(bbcode),		/* process shutdown */
	NULL,						/* request startup */
	NULL,						/* request shutdown */
	PHP_MINFO(bbcode),			/* extension info */
	PHP_BBCODE_VERSION,			/* extension version */
	STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_BBCODE
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(bbcode)
#endif

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */