/*
 * $Id$
 *
    Copyright (c) 2006-2019 Chung, Hyung-Hwan. All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:
    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.
    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
    NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "xli-prv.h"
#include <qse/cmn/chr.h>

static int get_token (qse_xli_t* xli);
static int read_list (qse_xli_t* xli, qse_xli_list_t* list, const qse_xli_scm_t* override);

enum
{
	TOK_STATUS_ENABLE_NSTR = (1 << 0)
};

static qse_xli_scm_t scm_val_iffy = { QSE_XLI_SCM_VALSTR | QSE_XLI_SCM_KEYNODUP, 1, 1 };

int qse_xli_openrstream (qse_xli_t* xli, qse_xli_io_arg_t* arg)
{
	qse_ssize_t n;

	n = xli->rio.impl (xli, QSE_XLI_IO_OPEN, arg, QSE_NULL, 0);
	if (n <= -1)
	{
		if (xli->errnum == QSE_XLI_ENOERR)
			qse_xli_seterrnum (xli, QSE_XLI_EIOUSR, QSE_NULL); 
		return -1;
	}

	return 0;
}

int qse_xli_closeactiverstream (qse_xli_t* xli)
{
	qse_ssize_t n;

	n = xli->rio.impl (xli, QSE_XLI_IO_CLOSE, xli->rio.inp, QSE_NULL, 0);
	if (n <= -1)
	{
		if (xli->errnum == QSE_XLI_ENOERR) 
			qse_xli_seterrnum (xli, QSE_XLI_EIOUSR, QSE_NULL);
		return -1;
	}

	return 0;
}

#define GET_CHAR(xli) \
	do { if (qse_xli_getchar(xli) <= -1) return -1; } while(0)

#define GET_CHAR_TO(xli,c) \
	do { \
		if (qse_xli_getchar(xli) <= -1) return -1; \
		c = (xli)->rio.last.c; \
	} while(0)

#define ADD_TOKEN_CHAR(xli,tok,c) \
	do { \
		if (qse_str_ccat((tok)->name,(c)) == (qse_size_t)-1) \
		{ \
			qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); \
			return -1; \
		} \
	} while (0)

#define ADD_TOKEN_STR(xli,tok,s,l) \
	do { \
		if (qse_str_ncat((tok)->name,(s),(l)) == (qse_size_t)-1) \
		{ \
			qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); \
			return -1; \
		} \
	} while (0)

#define SET_TOKEN_TYPE(xli,tok,code) \
	do { (tok)->type = (code); } while (0)

#define MATCH(xli,tok_type) ((xli)->tok.type == (tok_type))

typedef struct kwent_t kwent_t;

struct kwent_t 
{ 
	qse_cstr_t name;
	qse_xli_tok_type_t type; 
};

/* note that the keyword must start with @. */
static kwent_t kwtab[] = 
{
	/* keep it sorted by the first field for binary search */
	{ { QSE_T("@include"),     8 }, QSE_XLI_TOK_XINCLUDE }
};

static kwent_t boolkwtab[] = 
{
	/* keep it sorted by the first field for binary search */
	{ { QSE_T("false"),     5 }, QSE_XLI_TOK_FALSE },
	{ { QSE_T("true"),      4 }, QSE_XLI_TOK_TRUE }
};

int qse_xli_getchar (qse_xli_t* xli)
{
	qse_ssize_t n;
	qse_char_t lc, ec;

	if (xli->rio.inp->b.state == -1)
	{
		xli->rio.inp->b.state = 0;
		return -1;
	}
	else if (xli->rio.inp->b.state == 1)
	{
		xli->rio.inp->b.state = 0;
		goto return_eof;
	}

	if (xli->rio.inp->b.pos >= xli->rio.inp->b.len)
	{
		n = xli->rio.impl (
			xli, QSE_XLI_IO_READ, xli->rio.inp,
			xli->rio.inp->b.buf, QSE_COUNTOF(xli->rio.inp->b.buf)
		);
		if (n <= -1)
		{
			if (xli->errnum == QSE_XLI_ENOERR)
				qse_xli_seterrnum (xli, QSE_XLI_EIOUSR, QSE_NULL); 
			return -1;
		}

		if (n == 0)
		{
		return_eof:
			xli->rio.inp->last.c = QSE_CHAR_EOF;
			xli->rio.inp->last.line = xli->rio.inp->line;
			xli->rio.inp->last.colm = xli->rio.inp->colm;
			xli->rio.inp->last.file = xli->rio.inp->name;
			xli->rio.last = xli->rio.inp->last;
			return 0;
		}

		xli->rio.inp->b.pos = 0;
		xli->rio.inp->b.len = n;
	}

	if (xli->rio.inp->last.c == QSE_T('\n'))
	{
		/* if the previous charater was a newline,
		 * increment the line counter and reset column to 1.
		 * incrementing it line number here instead of
		 * updating inp->last causes the line number for
		 * QSE_XLI_TOK_EOF to be the same line as the last newline. */
		xli->rio.inp->line++;
		xli->rio.inp->colm = 1;
	}

	lc = xli->rio.inp->b.buf[xli->rio.inp->b.pos++];
	if (lc == QSE_T('\n') || lc == QSE_T('\r'))
	{
		/* handle common newline conventions.
		 *   LF+CR
		 *   CR+LF
		 *   LF
		 *   CR
		 * convert all above to a single LF.
		 */
		if (xli->rio.inp->b.pos >= xli->rio.inp->b.len)
		{
			n = xli->rio.impl (
				xli, QSE_XLI_IO_READ, xli->rio.inp,
				xli->rio.inp->b.buf, QSE_COUNTOF(xli->rio.inp->b.buf)
			);
			if (n <= -1)
			{
				xli->rio.inp->b.state = -1;
				goto done;
			}
			else if (n == 0)
			{
				xli->rio.inp->b.state = 1;
				goto done;
			}
			else
			{
				xli->rio.inp->b.pos = 0;
				xli->rio.inp->b.len = n;
			}
		}

		ec = (lc == '\n')? '\r': '\n';
		if (xli->rio.inp->b.buf[xli->rio.inp->b.pos] == ec) xli->rio.inp->b.pos++;

	done:
		lc = QSE_T('\n');
	}

	xli->rio.inp->last.c = lc;
	xli->rio.inp->last.line = xli->rio.inp->line;
	xli->rio.inp->last.colm = xli->rio.inp->colm++;
	xli->rio.inp->last.file = xli->rio.inp->name;
	xli->rio.last = xli->rio.inp->last;

	return 0;
}

static int skip_spaces (qse_xli_t* xli)
{
	qse_cint_t c = xli->rio.last.c;
	while (QSE_ISSPACE(c)) GET_CHAR_TO (xli, c);
	return 0;
}

static int skip_comment (qse_xli_t* xli, qse_xli_tok_t* tok)
{
	qse_cint_t c = xli->rio.last.c;

	if (c == QSE_T('#'))
	{
		/* skip up to \n */

		qse_str_clear (tok->name);

		do
		{ 
			GET_CHAR_TO (xli, c); 
			if (c == QSE_T('\n') || c == QSE_CHAR_EOF) break;

			if  (xli->opt.trait & QSE_XLI_KEEPTEXT) ADD_TOKEN_CHAR (xli, tok, c);
		}
		while (1);

		if ((xli->opt.trait & QSE_XLI_KEEPTEXT) && 
		    qse_xli_inserttext (xli, xli->parlink->list, QSE_NULL, QSE_STR_PTR(tok->name)) == QSE_NULL) return -1;

		GET_CHAR (xli); /* eat the new line letter */
		return 1; /* comment by # */
	}

	return 0; 
}

static qse_xli_tok_type_t classify_ident (qse_xli_t* xli, const qse_cstr_t* name)
{
	/* perform binary search */

	/* declaring left, right, mid to be the int type is ok
	 * because we know kwtab is small enough. */
	int left, right, mid;
	int bool_checked = 0;
	kwent_t* kwtabp;

	left = 0;
	right = QSE_COUNTOF(kwtab) - 1;
	kwtabp = kwtab;

retry:
	while (left <= right)
	{
		int n;
		kwent_t* kwp;

		/*mid = (left + right) / 2;*/
		mid = left + (right - left) / 2;
		kwp = &kwtabp[mid];

		/* the identifier classification is not affected by case-insensivity */
		n = qse_strxncmp(kwp->name.ptr, kwp->name.len, name->ptr, name->len);
		if (n > 0) 
		{
			/* if left, right, mid were of qse_size_t,
			 * you would need the following line. 
			if (mid == 0) break;
			 */
			right = mid - 1;
		}
		else if (n < 0) left = mid + 1;
		else return kwp->type;
	}

	if (!bool_checked && (xli->opt.trait & QSE_XLI_BOOLEAN))
	{
		bool_checked = 1;
		kwtabp = boolkwtab;
		left = 0;
		right = QSE_COUNTOF(boolkwtab) - 1;
		goto retry;
	}

	return QSE_XLI_TOK_IDENT;
}

static int get_symbols (qse_xli_t* xli, qse_cint_t c, qse_xli_tok_t* tok)
{
	struct ops_t
	{
		const qse_char_t* str;
		qse_size_t len;
		int tid;
	};

	static struct ops_t ops[] = 
	{
		{ QSE_T("="),   1, QSE_XLI_TOK_EQ          },
		{ QSE_T(","),   1, QSE_XLI_TOK_COMMA       },
		{ QSE_T(";"),   1, QSE_XLI_TOK_SEMICOLON   },
		{ QSE_T("{"),   1, QSE_XLI_TOK_LBRACE      },
		{ QSE_T("}"),   1, QSE_XLI_TOK_RBRACE      },
		{ QSE_NULL,     0, 0,              }
	};

	struct ops_t* p;
	int idx = 0;

	/* note that the loop below is not generaic enough.
	 * you must keep the operators strings in a particular order */


	for (p = ops; p->str != QSE_NULL; )
	{
		if (p->str[idx] == QSE_T('\0'))
		{
			ADD_TOKEN_STR (xli, tok, p->str, p->len);
			SET_TOKEN_TYPE (xli, tok, p->tid);
			return 1;
		}

		if (c == p->str[idx])
		{
			idx++;
			GET_CHAR_TO (xli, c);
			continue;
		}

		p++;
	}

	return 0;
}

static int end_include (qse_xli_t* xli, int noeof)
{
	int x;
	qse_xli_io_arg_t* cur;

	if (xli->rio.inp == &xli->rio.top) return 0; /* no include */


	/* if it is an included file, close it and
	 * retry to read a character from an outer file */

	x = xli->rio.impl (
		xli, QSE_XLI_IO_CLOSE, 
		xli->rio.inp, QSE_NULL, 0);

	/* if closing has failed, still destroy the
	 * sio structure first as normal and return
	 * the failure below. this way, the caller 
	 * does not call QSE_XLI_SIO_CLOSE on 
	 * xli->rio.inp again. */

	cur = xli->rio.inp;
	xli->rio.inp = xli->rio.inp->prev;

	QSE_ASSERT (cur->name != QSE_NULL);
	qse_xli_freemem (xli, cur);
	/* xli->parse.depth.incl--; */

	if ((xli->opt.trait & QSE_XLI_KEEPFILE) && !noeof && 
	    qse_xli_inserteof (xli, xli->parlink->list, QSE_NULL) == QSE_NULL) return -1;

	if (x != 0)
	{
		/* the failure mentioned above is returned here */
		if (xli->errnum == QSE_XLI_ENOERR)
			qse_xli_seterrnum (xli, QSE_XLI_EIOUSR, QSE_NULL); 
		return -1;
	}

	xli->rio.last = xli->rio.inp->last;
	return 1; /* ended the included file successfully */
}

static int begin_include (qse_xli_t* xli)
{
	qse_link_t* link;
	qse_xli_io_arg_t* arg = QSE_NULL;

	link = (qse_link_t*)qse_xli_callocmem(xli, 
		QSE_SIZEOF(*link) + QSE_SIZEOF(qse_char_t) * (QSE_STR_LEN(xli->tok.name) + 1));
	if (link == QSE_NULL) goto oops;

	qse_strncpy ((qse_char_t*)(link + 1), QSE_STR_PTR(xli->tok.name), QSE_STR_LEN(xli->tok.name));
	link->link = xli->rio_names;
	xli->rio_names = link;

	arg = (qse_xli_io_arg_t*)qse_xli_callocmem(xli, QSE_SIZEOF(*arg));
	if (arg == QSE_NULL) goto oops;

	arg->name = (const qse_char_t*)(link + 1);
	arg->line = 1;
	arg->colm = 1;

	/* let the argument's prev point field to the current */
	arg->prev = xli->rio.inp; 

	if (qse_xli_openrstream(xli, arg) <= -1) goto oops;

	/* i update the current pointer after opening is successful */
	xli->rio.inp = arg;
	/* xli->parse.depth.incl++; */

	/* read in the first character in the included file. 
	 * so the next call to get_token() sees the character read
	 * from this file. */
	if (qse_xli_getchar (xli) <= -1 || get_token (xli) <= -1) 
	{
		end_include (xli, 1); 
		/* i don't jump to oops since i've called 
		 * end_include() where xli->rio.inp/arg is freed. */
		return -1;
	}

	if ((xli->opt.trait & QSE_XLI_KEEPFILE) &&
	    qse_xli_insertfile (xli, xli->parlink->list, QSE_NULL, arg->name) == QSE_NULL) 
	{
		end_include (xli, 1);
		return -1;
	}

	return 0;

oops:
	/* i don't need to free 'link' since it's linked to
	 * xli->rio_names that's freed at the beginning of qse_xli_read()
	 * or by qse_xli_fini() */
	if (arg) qse_xli_freemem (xli, arg);
	return -1;
}


static int get_token_into (qse_xli_t* xli, qse_xli_tok_t* tok)
{
	qse_cint_t c;
	int n;
	int skip_semicolon_after_include = 0;

retry:
	do 
	{
		if (skip_spaces (xli) <= -1) return -1;
		if ((n = skip_comment (xli, tok)) <= -1) return -1; 
	} 
	while (n >= 1);

	qse_str_clear (tok->name);
	tok->loc.file = xli->rio.last.file;
	tok->loc.line = xli->rio.last.line;
	tok->loc.colm = xli->rio.last.colm;

	c = xli->rio.last.c;

	if (c == QSE_CHAR_EOF) 
	{
		n = end_include (xli, 0);
		if (n <= -1) return -1;
		if (n >= 1) 
		{
			/*xli->rio.last = xli->rio.inp->last;*/
			/* mark that i'm retrying after end of an included file */
			skip_semicolon_after_include = 1; 
			goto retry;
		}

		ADD_TOKEN_STR (xli, tok, QSE_T("<EOF>"), 5);
		SET_TOKEN_TYPE (xli, tok, QSE_XLI_TOK_EOF);
	}
	else if (c == QSE_T('@'))
	{
		/* keyword/directive - start with @ */

		int type;

		ADD_TOKEN_CHAR (xli, tok, c);
		GET_CHAR_TO (xli, c);

		if (!QSE_ISALPHA(c))
		{
			/* this directive is empty, not followed by a valid word */
			qse_xli_seterror (xli, QSE_XLI_EXKWEM, QSE_NULL, &tok->loc);
			return -1;
		}

		/* expect an identifier starting with an alphabet. the identifier 
		 * forming a keyword/directory is composed of alphabets. */
		do 
		{
			ADD_TOKEN_CHAR (xli, tok, c);
			GET_CHAR_TO (xli, c);
		} 
		while (QSE_ISALPHA(c));

		type = classify_ident (xli, QSE_STR_XSTR(tok->name));
		if (type == QSE_XLI_TOK_IDENT)
		{
			/* this keyword/directive is not recognized */
			qse_xli_seterror (xli, QSE_XLI_EXKWNR, QSE_STR_XSTR(tok->name), &tok->loc);
			return -1;
		}
		SET_TOKEN_TYPE (xli, tok, type);
	}
	else if (c == QSE_T('_') || QSE_ISALPHA(c) || 
	         (!(xli->tok_status & TOK_STATUS_ENABLE_NSTR) &&
	          (xli->opt.trait & QSE_XLI_LEADDIGIT) && 
	          QSE_ISDIGIT(c)))
	{
		int lead_digit = QSE_ISDIGIT(c);
		int all_digits = 1;
		qse_xli_tok_type_t type;

		/* a normal identifier can be composed of wider varieties of 
		 * characters than a keyword/directive */
		while (1)
		{
			ADD_TOKEN_CHAR (xli, tok, c);
			GET_CHAR_TO (xli, c);

			if (c == QSE_T('_') || c == QSE_T('-') || 
			    c == QSE_T(':') || c == QSE_T('*') ||
			    c == QSE_T('/') || QSE_ISALPHA(c)) 
			{
				all_digits = 0;
			}
			else if (QSE_ISDIGIT(c)) 
			{
				/* nothing to do */
			}
			else break;
		} 

		if (lead_digit && all_digits)
		{
			/* if an identifier begins with a digit, it must contain a non-digits character */
			qse_xli_seterror (xli, QSE_XLI_EIDENT, QSE_STR_XSTR(tok->name), &tok->loc);
			return -1;
		}

		type = classify_ident (xli, QSE_STR_XSTR(tok->name));
		SET_TOKEN_TYPE (xli, tok, type);
	}
	else if ((xli->tok_status & TOK_STATUS_ENABLE_NSTR) && QSE_ISDIGIT(c))
	{
		SET_TOKEN_TYPE (xli, tok, QSE_XLI_TOK_NSTR);
		do
		{
			ADD_TOKEN_CHAR (xli, tok, c);
			GET_CHAR_TO (xli, c);
		}
		while (QSE_ISDIGIT(c));
	}
	else if (c == QSE_T('\''))
	{
		/* single-quoted string - no escaping */
		SET_TOKEN_TYPE (xli, tok, QSE_XLI_TOK_SQSTR);

		while (1)
		{
			GET_CHAR_TO (xli, c);

			if (c == QSE_CHAR_EOF)
			{
				/* the string is not closed */
				qse_xli_seterror (xli, QSE_XLI_ESTRNC, QSE_NULL, &tok->loc);
				return -1;
			}

			if (c == QSE_T('\''))
			{
				/* terminating quote */
				GET_CHAR (xli);
				break;
			}

			ADD_TOKEN_CHAR (xli, tok, c);
		}
	}
	else if (c == QSE_T('\"'))
	{
		/* double-quoted string - support escaping */
		int escaped = 0;

		SET_TOKEN_TYPE (xli, tok, QSE_XLI_TOK_DQSTR);

		while (1)
		{
			GET_CHAR_TO (xli, c);

			if (c == QSE_CHAR_EOF)
			{
				/* the string is not closed */
				qse_xli_seterror (xli, QSE_XLI_ESTRNC, QSE_NULL, &tok->loc);
				return -1;
			}

			if (!escaped)
			{
				if (c == QSE_T('\\')) 
				{
					escaped = 1;
					continue;
				}

				if (c == QSE_T('\"'))
				{
					/* terminating quote */
					GET_CHAR (xli);
					break;
				}

				ADD_TOKEN_CHAR (xli, tok, c);
			}
			else
			{
				ADD_TOKEN_CHAR (xli, tok, c);
				escaped = 0;
			}
		}
	}
	else if ((xli->opt.trait & (QSE_XLI_KEYTAG | QSE_XLI_STRTAG)) && c == QSE_T('['))
	{
		/* a string tag is a bracketed word placed in front of a string value.
		 *   A = [tg] "abc"; 
		 * "tg" is stored into the tag field of qse_xli_str_t. 
		 */

		SET_TOKEN_TYPE (xli, tok, QSE_XLI_TOK_TAG);
		
		while (1)
		{
			GET_CHAR_TO (xli, c);

			if (c == QSE_CHAR_EOF || c == QSE_T('\n'))
			{
				/* the string tag is not closed */
				qse_xli_seterror (xli, QSE_XLI_ETAGNC, QSE_NULL, &xli->tok.loc);
				return -1;
			}

			if (c == QSE_T(']'))
			{
				/* terminating quote */
				GET_CHAR (xli);
				break;
			}

			if (!QSE_ISALNUM(c) && c != QSE_T('-') && c != QSE_T('_') && 
			    c != QSE_T(':') && c != QSE_T('*') && c != QSE_T('/') && 
			    c != QSE_T(',') && c != QSE_T('.') && c != QSE_T('|'))
			{
				qse_char_t cc = (qse_char_t)c;
				qse_cstr_t ea;
				ea.ptr = &cc;
				ea.len = 1;
				qse_xli_seterror (xli, QSE_XLI_ETAGCHR, &ea, &tok->loc);
				return -1;
			}

			ADD_TOKEN_CHAR (xli, tok, c);
		}
	}
	else
	{
		n = get_symbols (xli, c, tok);
		if (n <= -1) return -1; /* hard failure */
		if (n == 0)
		{
			/* not handled yet */
			if (c == QSE_T('\0'))
			{
				qse_cstr_t ea;
				ea.ptr = QSE_T("<NUL>");
				ea.len = 5;
				qse_xli_seterror (xli, QSE_XLI_ELXCHR, &ea, &tok->loc);
			}
			else
			{
				qse_char_t cc = (qse_char_t)c;
				qse_cstr_t ea;
				ea.ptr = &cc;
				ea.len = 1;
				qse_xli_seterror (xli, QSE_XLI_ELXCHR, &ea, &tok->loc);
			}
			return -1;
		}

		if (skip_semicolon_after_include && tok->type == QSE_XLI_TOK_SEMICOLON)
		{
			/* this handles the optional semicolon after the 
			 * included file named as in @include "file-name"; */
			skip_semicolon_after_include = 0;
			goto retry;
		}
	}

	if (skip_semicolon_after_include)
	{
		/* semiclon has not been skipped yet */
		qse_xli_seterror (xli, QSE_XLI_ESCOLON, QSE_STR_XSTR(tok->name), &tok->loc);
		return -1;
	}

	return 0;
}

static int get_token (qse_xli_t* xli)
{
	return get_token_into (xli, &xli->tok);
}

static int read_pair (qse_xli_t* xli, const qse_char_t* keytag, const qse_xli_scm_t* override)
{
	qse_cstr_t key;
	qse_xli_loc_t kloc;
	qse_char_t* name;
	qse_xli_pair_t* pair;
	qse_xli_list_t* parlist;
	qse_size_t dotted_curkey_len;
	qse_char_t* strtag;

	const qse_xli_scm_t* scm = QSE_NULL;
	int key_nodup = 0, key_alias = 0, val_iffy = 0;
	

	key.ptr = QSE_NULL;
	name = QSE_NULL;
	strtag = QSE_NULL;
	dotted_curkey_len = (qse_size_t)-1;

	parlist = xli->parlink->list;

	if (xli->opt.trait & QSE_XLI_KEYNODUP) key_nodup = 1;
	if (xli->opt.trait & QSE_XLI_KEYALIAS) key_alias = 1;

	kloc = xli->tok.loc;
	key.len = QSE_STR_LEN(xli->tok.name);
	key.ptr = qse_strdup (QSE_STR_PTR(xli->tok.name), qse_xli_getmmgr(xli));
	if (key.ptr == QSE_NULL) 
	{
		qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); 
		goto oops;
	}

	dotted_curkey_len = QSE_STR_LEN (xli->dotted_curkey);
	if ((dotted_curkey_len > 0 && qse_str_cat (xli->dotted_curkey, QSE_T(".")) == (qse_size_t)-1) ||
	    qse_str_cat (xli->dotted_curkey, key.ptr) == (qse_size_t)-1)
	{
		qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); 
		goto oops;
	}

	if (xli->opt.trait & QSE_XLI_VALIDATE) 
	{
		if (override) scm = override;
		else
		{
			qse_rbt_pair_t* pair;

			pair = qse_rbt_search (xli->schema, QSE_STR_PTR(xli->dotted_curkey), QSE_STR_LEN(xli->dotted_curkey));
			if (pair == QSE_NULL)
			{
				qse_xli_seterror (xli, QSE_XLI_EUDKEY, (const qse_cstr_t*)&key, &kloc);
				goto oops;
			}

			scm = (qse_xli_scm_t*)QSE_RBT_VPTR(pair);
		}

		if (scm->flags & QSE_XLI_SCM_KEYNODUP) key_nodup = 2;
		if (scm->flags & QSE_XLI_SCM_KEYALIAS) key_alias = 2;
		if (scm->flags & QSE_XLI_SCM_VALIFFY) val_iffy = 1;
	}

	if (key_nodup)
	{
		qse_xli_atom_t* atom;

		/* find any key conflicts in the current scope */
		/* TODO: optimization. no sequential search */
		atom = parlist->tail;
		while (atom)
		{
			if (atom->type == QSE_XLI_PAIR &&
			    xli->opt.strcmp(((qse_xli_pair_t*)atom)->key, QSE_STR_PTR(xli->tok.name)) == 0)
			{
				qse_xli_seterror (xli, QSE_XLI_EEXIST, QSE_STR_XSTR(xli->tok.name), &xli->tok.loc);
				goto oops;
			}

			atom = atom->prev;
		}
	}

	/* once the key name is read, enable the numeric string for a value */
	xli->tok_status |= TOK_STATUS_ENABLE_NSTR;

	if (get_token (xli) <= -1) goto oops;

	if (key_alias)
	{
		/* the alias part must be unique for the same key(s) */
		if (MATCH(xli, QSE_XLI_TOK_IDENT) || MATCH(xli, QSE_XLI_TOK_DQSTR) || MATCH(xli, QSE_XLI_TOK_SQSTR) || MATCH(xli, QSE_XLI_TOK_NSTR))
		{
			qse_xli_atom_t* atom;

			atom = parlist->tail;
			while (atom)
			{
				if (atom->type == QSE_XLI_PAIR &&
				    ((qse_xli_pair_t*)atom)->alias && 
				    xli->opt.strcmp(((qse_xli_pair_t*)atom)->key, key.ptr) == 0 &&
				    xli->opt.strcmp(((qse_xli_pair_t*)atom)->alias, QSE_STR_PTR(xli->tok.name)) == 0)
				{
					qse_xli_seterror (xli, QSE_XLI_EEXIST, QSE_STR_XSTR(xli->tok.name), &xli->tok.loc);
					goto oops;
				}
				atom = atom->prev;
			}

			name = qse_strdup (QSE_STR_PTR(xli->tok.name), qse_xli_getmmgr(xli));
			if (name == QSE_NULL) 
			{
				qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); 
				goto oops;
			}

			if (get_token (xli) <= -1) goto oops;
		}
		else if (key_alias == 2)
		{
			/* SCM_KEYALIAS is specified for this particular item. Let the alias be required. 
			 * If KEYALIAS is globally specified with the specific one, it's optional. */
			qse_xli_seterrnum (xli, QSE_XLI_ENOALI, (const qse_cstr_t*)&key); 
			goto oops;
		}
	}

	if (MATCH(xli, QSE_XLI_TOK_EQ))
	{
		if (get_token (xli) <= -1) goto oops;

		if ((xli->opt.trait & QSE_XLI_STRTAG) && MATCH(xli, QSE_XLI_TOK_TAG))
		{
			strtag = qse_strxdup (QSE_STR_PTR(xli->tok.name), QSE_STR_LEN(xli->tok.name), qse_xli_getmmgr(xli));
			if (strtag == QSE_NULL)
			{
				qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); 
				goto oops;
			}

			if (get_token (xli) <= -1) goto oops;
		}

		if (MATCH(xli, QSE_XLI_TOK_TRUE) || MATCH(xli, QSE_XLI_TOK_FALSE))
		{
			qse_xli_val_t* v;

			v = MATCH(xli, QSE_XLI_TOK_TRUE)? (qse_xli_val_t*)&xli->root->xtrue:
			                                  (qse_xli_val_t*)&xli->root->xfalse;
			pair = qse_xli_insertpair(xli, parlist, QSE_NULL, key.ptr, name, keytag, v);
			if (pair == QSE_NULL) goto oops;

			if (xli->opt.cbs.pair_read) xli->opt.cbs.pair_read (xli, pair, &kloc);

			if (get_token (xli) <= -1) goto oops; /* skip the value */

			if (!MATCH(xli, QSE_XLI_TOK_SEMICOLON))
			{
				qse_xli_seterror (xli, QSE_XLI_ESCOLON, QSE_STR_XSTR(xli->tok.name), &xli->tok.loc);
				goto oops;
			}

			if (get_token (xli) <= -1) goto oops; /* skip the value */
		}
		else if (MATCH(xli, QSE_XLI_TOK_SQSTR) || MATCH(xli, QSE_XLI_TOK_DQSTR) || MATCH(xli, QSE_XLI_TOK_NSTR) || MATCH(xli, QSE_XLI_TOK_IDENT))
		{
			qse_xli_str_t* curstrseg;
			qse_size_t segcount = 0;

			if (scm && !(scm->flags & QSE_XLI_SCM_VALSTR))
			{
				/* check the value type */
				qse_xli_seterror (xli, QSE_XLI_EILVAL, (const qse_cstr_t*)&key, &kloc);
				goto oops;
			}

			/* add a new pair with the initial string segment */
			pair = qse_xli_insertpairwithstr(xli, parlist, QSE_NULL, key.ptr, name, keytag, QSE_STR_XSTR(xli->tok.name), strtag);
			if (pair == QSE_NULL) goto oops;

			if (xli->opt.cbs.pair_read) xli->opt.cbs.pair_read (xli, pair, &kloc);

			if (MATCH(xli, QSE_XLI_TOK_NSTR))
			{
				((qse_xli_str_t*)pair->val)->flags |= QSE_XLI_STR_NSTR;
			}

			segcount++;
			curstrseg = (qse_xli_str_t*)pair->val;

			if (get_token (xli) <= -1) goto oops;
			if ((xli->opt.trait & QSE_XLI_MULSEGSTR) && MATCH(xli, QSE_XLI_TOK_COMMA))
			{
				/* multi-segmented string */
				do
				{
					if (get_token (xli) <= -1) goto oops; /* skip the comma */

					if (strtag) 
					{
						qse_xli_freemem (xli, strtag);
						strtag = QSE_NULL;
					}

					if ((xli->opt.trait & QSE_XLI_STRTAG) && MATCH(xli, QSE_XLI_TOK_TAG))
					{
						strtag = qse_strxdup (QSE_STR_PTR(xli->tok.name), QSE_STR_LEN(xli->tok.name), qse_xli_getmmgr(xli));
						if (strtag == QSE_NULL)
						{
							qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); 
							goto oops;
						}
						
						if (get_token (xli) <= -1) goto oops; 
					}

					if (!MATCH(xli, QSE_XLI_TOK_SQSTR) && !MATCH(xli, QSE_XLI_TOK_DQSTR) && !MATCH(xli, QSE_XLI_TOK_NSTR) && !MATCH(xli, QSE_XLI_TOK_IDENT))
					{
						qse_xli_seterror (xli, QSE_XLI_ESYNTAX, QSE_NULL, &xli->tok.loc);
						goto oops;
					}

					/* add an additional segment to the string */
					curstrseg = qse_xli_addsegtostr (xli, curstrseg, strtag, QSE_STR_XSTR(xli->tok.name));
					if (curstrseg == QSE_NULL) goto oops;

					segcount++;
					if (get_token (xli) <= -1) goto oops; /* skip the value */
				}
				while (MATCH(xli, QSE_XLI_TOK_COMMA));
			}

			/* semicolon is mandatory for a string */
			if (!MATCH(xli, QSE_XLI_TOK_SEMICOLON))
			{
				qse_xli_seterror (xli, QSE_XLI_ESCOLON, QSE_STR_XSTR(xli->tok.name), &xli->tok.loc);
				goto oops;
			}

			if (scm && (segcount < scm->str_minseg || segcount > scm->str_maxseg))
			{
				/* too many string segments for the key */
				qse_xli_seterror (xli, QSE_XLI_ESTRSEG, (const qse_cstr_t*)&key, &kloc);
				goto oops;
			}

			/* semicolon read. turn off NSTR */
			xli->tok_status &= ~TOK_STATUS_ENABLE_NSTR;
			if (get_token(xli) <= -1) goto oops;
		}
		else
		{
			qse_xli_seterror (xli, QSE_XLI_EVALUE, QSE_STR_XSTR(xli->tok.name), &xli->tok.loc);
			goto oops;
		}
		/* TODO: check against schema */
	}
	else if (!(xli->opt.trait & QSE_XLI_NOLIST) && MATCH(xli, QSE_XLI_TOK_LBRACE))
	{
		if (scm && !(scm->flags & QSE_XLI_SCM_VALLIST))
		{
			/* check the value type */
			qse_xli_seterror (xli, QSE_XLI_EILVAL, (const qse_cstr_t*)&key, &kloc);
			goto oops;
		}

		xli->tok_status &= ~TOK_STATUS_ENABLE_NSTR;

		/* insert a pair with an empty list */
		pair = qse_xli_insertpairwithemptylist(xli, parlist, QSE_NULL, key.ptr, name, keytag);
		if (pair == QSE_NULL) goto oops;

		if (xli->opt.cbs.pair_read) xli->opt.cbs.pair_read (xli, pair, &kloc);

		/* skip validations of child pairs if the schema for the 
		 * current pair is set with QSE_XLI_SCM_VALIFFY. 
		 * the schema for the child pairs, if specified, must not 
		 * take effect. */
		if (read_list(xli, (qse_xli_list_t*)pair->val, (val_iffy? &scm_val_iffy: QSE_NULL)) <= -1) goto oops;

		if (!MATCH(xli, QSE_XLI_TOK_RBRACE))
		{
			qse_xli_seterror (xli, QSE_XLI_ERBRACE, QSE_STR_XSTR(xli->tok.name), &xli->tok.loc);
			goto oops;
		}

		if (get_token(xli) <= -1) goto oops;

		/* semicolon is optional for a list */
		if (MATCH(xli, QSE_XLI_TOK_SEMICOLON))
		{
			/* skip the semicolon */
			if (get_token (xli) <= -1) goto oops;
		}

		/* TODO: check against schema */
	}
	else if (MATCH(xli, QSE_XLI_TOK_SEMICOLON))
	{
		if (xli->opt.trait & QSE_XLI_NONIL) 
		{
			qse_xli_seterror (xli, QSE_XLI_ENOVAL, (const qse_cstr_t*)&key, &kloc);
			goto oops;
		}

		if (scm && !(scm->flags & QSE_XLI_SCM_VALNIL) &&
		           !((scm->flags & QSE_XLI_SCM_VALSTR) && scm->str_minseg <= 0))
		{
			/* check the value type */
			qse_xli_seterror (xli, QSE_XLI_ENOVAL, (const qse_cstr_t*)&key, &kloc);
			goto oops;
		}

		xli->tok_status &= ~TOK_STATUS_ENABLE_NSTR;

		/* no value has been specified for the pair */
		pair = qse_xli_insertpair (xli, parlist, QSE_NULL, key.ptr, name, keytag, (qse_xli_val_t*)&xli->root->xnil);
		if (pair == QSE_NULL) goto oops;

		if (xli->opt.cbs.pair_read) xli->opt.cbs.pair_read (xli, pair, &kloc);

		/* skip the semicolon */
		if (get_token (xli) <= -1) goto oops;

		/* TODO: check against schema */
	}
	else
	{
		qse_xli_seterror (xli, QSE_XLI_ELBREQ, QSE_STR_XSTR(xli->tok.name), &xli->tok.loc);
		goto oops;
	}

	if (strtag) qse_xli_freemem (xli, strtag);
	qse_xli_freemem (xli, name);
	qse_xli_freemem (xli, key.ptr);
	qse_str_setlen (xli->dotted_curkey, dotted_curkey_len);
	return 0;
	
oops:
	xli->tok_status &= ~TOK_STATUS_ENABLE_NSTR;
	if (strtag) qse_xli_freemem (xli, strtag);
	if (name) qse_xli_freemem (xli, name);
	if (key.ptr) qse_xli_freemem (xli, key.ptr);
	if (dotted_curkey_len != (qse_size_t)-1)
		qse_str_setlen (xli->dotted_curkey, dotted_curkey_len);
	return -1;
}

qse_xli_list_link_t* qse_xli_makelistlink (qse_xli_t* xli, qse_xli_list_t* parlist)
{
	qse_xli_list_link_t* link;

	link = (qse_xli_list_link_t*)qse_xli_callocmem(xli, QSE_SIZEOF(*link));
	if (link == QSE_NULL) return QSE_NULL;

	link->list = parlist;
	link->next = xli->parlink;
	xli->parlink = link;

	return link;
}

void qse_xli_freelistlink (qse_xli_t* xli, qse_xli_list_link_t* link)
{
	QSE_ASSERT (xli->parlink == link); 
	xli->parlink = link->next;
	qse_xli_freemem (xli, link);
}

static int __read_list (qse_xli_t* xli, const qse_xli_scm_t* override)
{
	while (1)
	{
		if (MATCH(xli, QSE_XLI_TOK_XINCLUDE))
		{
			if (get_token(xli) <= -1) return -1;

			if (!MATCH(xli,QSE_XLI_TOK_SQSTR) && !MATCH(xli,QSE_XLI_TOK_DQSTR))
			{
				qse_xli_seterror (xli, QSE_XLI_EINCLSTR, QSE_NULL, &xli->tok.loc);
				return -1;
			}

			if (begin_include (xli) <= -1) return -1;
		}
		else if ((xli->opt.trait & QSE_XLI_KEYTAG) && MATCH(xli, QSE_XLI_TOK_TAG))
		{
			qse_char_t* keytag;
			int x;

			keytag = qse_strxdup (QSE_STR_PTR(xli->tok.name), QSE_STR_LEN(xli->tok.name), qse_xli_getmmgr(xli));
			if (keytag == QSE_NULL)
			{
				qse_xli_seterrnum (xli, QSE_XLI_ENOMEM, QSE_NULL); 
				return -1;
			}

			if (get_token(xli) <= -1) 
			{
				qse_xli_freemem (xli, keytag);
				return -1;
			}

			if (!MATCH(xli, QSE_XLI_TOK_IDENT) && !MATCH(xli, QSE_XLI_TOK_DQSTR) && !MATCH(xli, QSE_XLI_TOK_SQSTR))
			{
				qse_xli_freemem (xli, keytag);
				qse_xli_seterror (xli, QSE_XLI_ENOKEY, QSE_NULL, &xli->tok.loc);
				return -1;
			}

			x = read_pair(xli, keytag, override);
			qse_xli_freemem (xli, keytag);
			if (x <= -1) return -1;
		}
		else if (MATCH(xli, QSE_XLI_TOK_IDENT) || MATCH(xli, QSE_XLI_TOK_DQSTR) || MATCH(xli, QSE_XLI_TOK_SQSTR))
		{
			if (read_pair(xli, QSE_NULL, override) <= -1) return -1;
		}
		else if (MATCH(xli, QSE_XLI_TOK_TEXT))
		{
			if (get_token(xli) <= -1) return -1;
		}
		else 
		{
			break;
		}
	}

	return 0;
}

static int read_list (qse_xli_t* xli, qse_xli_list_t* parlist, const qse_xli_scm_t* override)
{
	qse_xli_list_link_t* link;

	link = qse_xli_makelistlink(xli, parlist);
	if (link == QSE_NULL) return -1;

	QSE_ASSERT (link == xli->parlink);

	/* get_token() here is to read the token after the left brace.
	 * it must be called after the xli->parlink has been updated
	 * in case there are comments at the beginning of the list */
	if (get_token(xli) <= -1 || __read_list(xli, override) <= -1) 
	{
		qse_xli_freelistlink (xli, link);
		return -1;
	}

	QSE_ASSERT (link == xli->parlink);
	qse_xli_freelistlink (xli, link);

	return 0;
}

static int read_root_list (qse_xli_t* xli)
{
	qse_xli_list_link_t* link;

	link = qse_xli_makelistlink (xli, &xli->root->list);
	if (!link) return -1;

	QSE_ASSERT (link == xli->parlink);

	if (qse_xli_getchar(xli) <= -1 || get_token(xli) <= -1 || __read_list(xli, QSE_NULL) <= -1)
	{
		qse_xli_freelistlink (xli, link);
		return -1;
	}

	QSE_ASSERT (link == xli->parlink);
	qse_xli_freelistlink (xli, link);

	return 0;
}

void qse_xli_clearrionames (qse_xli_t* xli)
{
	qse_link_t* cur;
	while (xli->rio_names)
	{
		cur = xli->rio_names;
		xli->rio_names = cur->link;
		qse_xli_freemem (xli, cur);
	}
}

int qse_xli_read (qse_xli_t* xli, qse_xli_io_impl_t io)
{
	if (!io)
	{
		qse_xli_seterrnum (xli, QSE_XLI_EINVAL, QSE_NULL); 
		return -1;
	}

	QSE_MEMSET (&xli->rio, 0, QSE_SIZEOF(xli->rio));
	xli->rio.impl = io;
	xli->rio.top.line = 1;
	xli->rio.top.colm = 1;
	xli->rio.inp = &xli->rio.top;

	xli->tok_status = 0;

	qse_xli_seterrnum (xli, QSE_XLI_ENOERR, QSE_NULL); 
	qse_xli_clearrionames (xli);

	QSE_ASSERT (QSE_STR_LEN(xli->dotted_curkey) == 0);

	if (qse_xli_openrstream (xli, xli->rio.inp) <= -1) return -1;
	/* the input stream is open now */

	if (read_root_list (xli) <= -1) goto oops;

	QSE_ASSERT (xli->parlink == QSE_NULL);

	if (!MATCH(xli, QSE_XLI_TOK_EOF))
	{
		qse_xli_seterror (xli, QSE_XLI_ESYNTAX, QSE_NULL, &xli->tok.loc);
		goto oops;
	}

	QSE_ASSERT (xli->rio.inp == &xli->rio.top);
	qse_xli_closeactiverstream (xli);
	qse_str_clear (xli->tok.name);
	return 0;

oops:
	/* an error occurred and control has reached here
	 * probably, some included files might not have been 
	 * closed. close them */
	while (xli->rio.inp != &xli->rio.top)
	{
		qse_xli_io_arg_t* prev;

		/* nothing much to do about a close error */
		qse_xli_closeactiverstream (xli);

		prev = xli->rio.inp->prev;
		QSE_ASSERT (xli->rio.inp->name != QSE_NULL);
		qse_xli_freemem (xli, xli->rio.inp);
		xli->rio.inp = prev;
	}
	
	qse_xli_closeactiverstream (xli);
	qse_str_clear (xli->tok.name);
	return -1;
}
