/*
	qfplist.c

	Property list management

	Copyright (C) 2000  Jeff Teunissen <deek@d2dc.net>

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

	See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

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

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#if defined(_WIN32) && defined(HAVE_MALLOC_H)
#include <malloc.h>
#endif

#include "qfalloca.h"

#include "QF/dstring.h"
#include "QF/hash.h"
#include "QF/qfplist.h"
#include "QF/qtypes.h"
#include "QF/sys.h"

/*
	Generic property list item.
*/
struct plitem_s {
	pltype_t	type;
	void		*data;
};

/*
	Dictionaries
*/
struct dictkey_s {
	char		*key;
	plitem_t	*value;
};
typedef struct dictkey_s	dictkey_t;

/*
	Arrays
*/
struct plarray_s {
	int				numvals;		///< Number of items in array
	int				maxvals;		///< Number of items that can be stored
									///< before a realloc is necesary.
	struct plitem_s **values;		///< Array data
};
typedef struct plarray_s	plarray_t;

/*
	Typeless, unformatted binary data
*/
struct plbinary_s {
	size_t		size;
	void		*data;
};
typedef struct plbinary_s	plbinary_t;

typedef struct pldata_s {	// Unparsed property list string
	const char		*ptr;
	unsigned int	end;
	unsigned int	pos;
	unsigned int	line;
	const char		*error;
} pldata_t;

//	Ugly defines for fast checking and conversion from char to number
#define inrange(ch,min,max) ((ch) >= (min) && (ch) <= (max))
#define char2num(ch) \
	(inrange((ch), '0', '9') ? ((ch) - '0') \
							 : 10 + (inrange((ch), 'a', 'f') ? ((ch) - 'a') \
															 : ((ch) - 'A')))

static byte quotable_bitmap[32];
static inline int
is_quotable (byte x)
{
	return quotable_bitmap[x / 8] & (1 << (x % 8));
}

static void
init_quotables (void)
{
	const char *unquotables = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
							  "abcdefghijklmnopqrstuvwxyz!#$%&*+-./:?@|~_^";
	const byte *c;
	memset (quotable_bitmap, ~0, sizeof (quotable_bitmap));
	for (c = (byte *) unquotables; *c; c++)
		quotable_bitmap[*c / 8] &= ~(1 << (*c % 8));
}

static plitem_t *PL_ParsePropertyListItem (pldata_t *);
static qboolean PL_SkipSpace (pldata_t *);
static char *PL_ParseQuotedString (pldata_t *);
static char *PL_ParseUnquotedString (pldata_t *);
static char *PL_ParseData (pldata_t *, int *);

static const char *
dict_get_key (const void *i, void *unused)
{
	dictkey_t	*item = (dictkey_t *) i;
	return item->key;
}

static void
dict_free (void *i, void *unused)
{
	dictkey_t	*item = (dictkey_t *) i;
	free (item->key);
	if (item->value)		// Make descended stuff get freed
		PL_Free (item->value);
	free (item);
}

static plitem_t *
PL_NewItem (pltype_t type)
{
	plitem_t   *item = calloc (1, sizeof (plitem_t));
	item->type = type;
	return item;
}

VISIBLE plitem_t *
PL_NewDictionary (void)
{
	plitem_t   *item = PL_NewItem (QFDictionary);
	hashtab_t  *dict = Hash_NewTable (1021, dict_get_key, dict_free, NULL);
	item->data = dict;
	return item;
}

VISIBLE plitem_t *
PL_NewArray (void)
{
	plitem_t   *item = PL_NewItem (QFArray);
	plarray_t  *array = calloc (1, sizeof (plarray_t));
	item->data = array;
	return item;
}

VISIBLE plitem_t *
PL_NewData (void *data, size_t size)
{
	plitem_t   *item = PL_NewItem (QFBinary);
	plbinary_t *bin = malloc (sizeof (plbinary_t));
	item->data = bin;
	bin->data = data;
	bin->size = size;
	return item;
}

static plitem_t *
new_string (char *str)
{
	plitem_t   *item = PL_NewItem (QFString);
	item->data = str;
	return item;
}

VISIBLE plitem_t *
PL_NewString (const char *str)
{
	return new_string (strdup (str));
}

VISIBLE void
PL_Free (plitem_t *item)
{
	switch (item->type) {
		case QFDictionary:
			Hash_DelTable (item->data);
			break;

		case QFArray: {
				int 	i = ((plarray_t *) item->data)->numvals;

				while (i-- > 0) {
					PL_Free (((plarray_t *) item->data)->values[i]);
				}
				free (((plarray_t *) item->data)->values);
				free (item->data);
			}
			break;

		case QFBinary:
			free (((plbinary_t *) item->data)->data);
			free (item->data);
			break;

		case QFString:
			free (item->data);
			break;
	}
	free (item);
}

VISIBLE const char *
PL_String (plitem_t *string)
{
	if (string->type != QFString)
		return NULL;
	return string->data;
}

VISIBLE plitem_t *
PL_ObjectForKey (plitem_t *dict, const char *key)
{
	hashtab_t  *table = (hashtab_t *) dict->data;
	dictkey_t  *k;

	if (dict->type != QFDictionary)
		return NULL;

	k = (dictkey_t *) Hash_Find (table, key);
	return k ? k->value : NULL;
}

VISIBLE plitem_t *
PL_RemoveObjectForKey (plitem_t *dict, const char *key)
{
	hashtab_t  *table = (hashtab_t *) dict->data;
	dictkey_t  *k;
	plitem_t   *value;

	if (dict->type != QFDictionary)
		return NULL;

	k = (dictkey_t *) Hash_Del (table, key);
	if (!k)
		return NULL;
	value = k->value;
	k->value = 0;
	dict_free (k, 0);
	return value;
}

VISIBLE plitem_t *
PL_D_AllKeys (plitem_t *dict)
{
	void		**list, **l;
	dictkey_t	*current;
	plitem_t	*array;

	if (dict->type != QFDictionary)
		return NULL;

	if (!(l = list = Hash_GetList ((hashtab_t *) dict->data)))
		return NULL;

	if (!(array = PL_NewArray ()))
		return NULL;

	while ((current = (dictkey_t *) *l++)) {
		PL_A_AddObject (array, PL_NewString (current->key));
	}
	free (list);

	return array;
}

VISIBLE int
PL_D_NumKeys (plitem_t *dict)
{
	if (dict->type != QFDictionary)
		return 0;
	return Hash_NumElements ((hashtab_t *) dict->data);
}

VISIBLE plitem_t *
PL_ObjectAtIndex (plitem_t *array, int index)
{
	plarray_t  *arr = (plarray_t *) array->data;

	if (array->type != QFArray)
		return NULL;

	return index >= 0 && index < arr->numvals ? arr->values[index] : NULL;
}

VISIBLE qboolean
PL_D_AddObject (plitem_t *dict, const char *key, plitem_t *value)
{
	dictkey_t	*k;

	if (dict->type != QFDictionary)
		return false;

	if ((k = Hash_Find ((hashtab_t *)dict->data, key))) {
		PL_Free ((plitem_t *) k->value);
		k->value = value;
	} else {
		k = malloc (sizeof (dictkey_t));

		if (!k)
			return false;

		k->key = strdup (key);
		k->value = value;

		Hash_Add ((hashtab_t *)dict->data, k);
	}
	return true;
}

VISIBLE qboolean
PL_A_InsertObjectAtIndex (plitem_t *array, plitem_t *item, int index)
{
	plarray_t  *arr;

	if (array->type != QFArray)
		return false;

	arr = (plarray_t *)array->data;

	if (arr->numvals == arr->maxvals) {
		int         size = (arr->maxvals + 128) * sizeof (plitem_t *);
		plitem_t  **tmp = realloc (arr->values, size);

		if (!tmp)
			return false;

		arr->maxvals += 128;
		arr->values = tmp;
		memset (arr->values + arr->numvals, 0,
				(arr->maxvals - arr->numvals) * sizeof (plitem_t *));
	}

	if (index == -1)
		index = arr->numvals;

	if (index < 0 || index > arr->numvals)
		return false;

	memmove (arr->values + index + 1, arr->values + index,
			 (arr->numvals - index) * sizeof (plitem_t *));
	arr->values[index] = item;
	arr->numvals++;
	return true;
}

VISIBLE qboolean
PL_A_AddObject (plitem_t *array, plitem_t *item)
{
	return PL_A_InsertObjectAtIndex (array, item, -1);
}

VISIBLE int
PL_A_NumObjects (plitem_t *array)
{
	if (array->type != QFArray)
		return 0;
	return ((plarray_t *) array->data)->numvals;
}

VISIBLE plitem_t *
PL_RemoveObjectAtIndex (plitem_t *array, int index)
{
	plarray_t  *arr;
	plitem_t   *item;

	if (array->type != QFArray)
		return 0;

	arr = (plarray_t *)array->data;

	if (index < 0 || index >= arr->numvals)
		return 0;

	item = arr->values[index];
	arr->numvals--;
	while (index < arr->numvals) {
		arr->values[index] = arr->values[index + 1];
		index++;
	}

	return item;
}

static qboolean
PL_SkipSpace (pldata_t *pl)
{
	while (pl->pos < pl->end) {
		char	c = pl->ptr[pl->pos];

		if (!isspace ((byte) c)) {
			if (c == '/' && pl->pos < pl->end - 1) {	// check for comments
				if (pl->ptr[pl->pos + 1] == '/') {
					pl->pos += 2;

					while (pl->pos < pl->end) {
						c = pl->ptr[pl->pos];

						if (c == '\n')
							break;
						pl->pos++;
					}
					if (pl->pos >= pl->end) {
						pl->error = "Reached end of string in comment";
						return false;
					}
				} else if (pl->ptr[pl->pos + 1] == '*') {	// "/*" comments
					pl->pos += 2;

					while (pl->pos < pl->end) {
						c = pl->ptr[pl->pos];

						if (c == '\n') {
							pl->line++;
						} else if (c == '*' && pl->pos < pl->end - 1
									&& pl->ptr[pl->pos+1] == '/') {
							pl->pos++;
							break;
						}
						pl->pos++;
					}
					if (pl->pos >= pl->end) {
						pl->error = "Reached end of string in comment";
						return false;
					}
				} else {
					return true;
				}
			} else {
				return true;
			}
		}
		if (c == '\n') {
			pl->line++;
		}
		pl->pos++;
	}
	pl->error = "Reached end of string";
	return false;
}

static inline byte
from_hex (byte a)
{
	if (a >= '0' && a <= '9')
		return a - '0';
	if (a >= 'A' && a <= 'F')
		return a - 'A' + 10;
	return a - 'a' + 10;
}

static inline byte
make_byte (byte h, byte l)
{
	return (from_hex (h) << 4) | from_hex (l);
}

static char *
PL_ParseData (pldata_t *pl, int *len)
{
	unsigned    start = ++pl->pos;
	int         nibbles = 0, i;
	char       *str, c;

	while (pl->pos < pl->end) {
		c = pl->ptr[pl->pos++];
		if (isxdigit ((byte) c)) {
			nibbles++;
			continue;
		}
		if (c == '>') {
			if (nibbles & 1) {
				pl->error = "invalid data, missing nibble";
				return NULL;
			}
			*len = nibbles / 2;
			str = malloc (*len);
			for (i = 0; i < *len; i++)
				str[i] = make_byte (pl->ptr[start + i * 2],
									pl->ptr[start + i * 2 + 1]);
			return str;
		}
		pl->error = "invalid character in data";
		return NULL;
	}
	pl->error = "Reached end of string while parsing data";
	return NULL;
}

static char *
PL_ParseQuotedString (pldata_t *pl)
{
	unsigned int	start = ++pl->pos;
	unsigned int	escaped = 0;
	unsigned int	shrink = 0;
	qboolean		hex = false;
	qboolean        long_string = false;
	char			*str;

	if (pl->ptr[pl->pos] == '"' &&
		pl->ptr[pl->pos + 1] == '"') {
		long_string = true;
		start += 2;
		pl->pos += 2;
	}

	while (pl->pos < pl->end) {

		char	c = pl->ptr[pl->pos];

		if (escaped) {
			if (escaped == 1 && inrange (c, '0', '7')) {
				escaped++;
				hex = false;
			} else if (escaped > 1) {
				if (escaped == 2 && c == 'x') {
					hex = true;
					shrink++;
					escaped++;
				} else if (hex && inrange (escaped, 3, 4)
						   && isxdigit ((byte) c)) {
					shrink++;
					escaped++;
				} else if (inrange (escaped, 1, 3) && inrange (c, '0', '7')) {
					shrink++;
					escaped++;
				} else {
					pl->pos--;
					escaped = 0;
				}
			} else {
				escaped = 0;
			}
		} else {
			if (c == '\\') {
				escaped = 1;
				shrink++;
			} else if (c == '"'
					   && (!long_string || (pl->ptr[pl->pos + 1] == '"'
											&& pl->ptr[pl->pos + 2] == '"'))) {
				break;
			}
		}

		if (c == '\n') {
			pl->line++;
		}

		pl->pos++;
	}

	if (pl->pos >= pl->end) {
		pl->error = "Reached end of string while parsing quoted string";
		return NULL;
	}

	if (pl->pos - start - shrink == 0) {
		str = strdup ("");
	} else {
		char			*chars = alloca(pl->pos - start - shrink);
		unsigned int	j;
		unsigned int	k;

		escaped = 0;
		hex = false;

		for (j = start, k = 0; j < pl->pos; j++) {

			char	c = pl->ptr[j];

			if (escaped) {
				if (escaped == 1 && inrange (c, '0', '7')) {
					chars[k] = c - '0';
					hex = false;
					escaped++;
				} else if (escaped > 1) {
					if (escaped == 2 && c == 'x') {
						hex = true;
						escaped++;
					} else if (hex && inrange (escaped, 3, 4)
							   && isxdigit ((byte) c)) {
						chars[k] <<= 4;
						chars[k] |= char2num (c);
						escaped++;
					} else if (inrange (escaped, 1, 3)
							   && inrange (c, '0', '7')) {
						chars[k] <<= 3;
						chars[k] |= (c - '0');
						escaped++;
					} else {
						escaped = 0;
						j--;
						k++;
					}
				} else {
					escaped = 0;
					switch (c) {
						case 'a':
							chars[k] = '\a';
							break;
						case 'b':
							chars[k] = '\b';
							break;
						case 't':
							chars[k] = '\t';
							break;
						case 'r':
							chars[k] = '\r';
							break;
						case 'n':
							chars[k] = '\n';
							break;
						case 'v':
							chars[k] = '\v';
							break;
						case 'f':
							chars[k] = '\f';
							break;
						default:
							chars[k] = c;
							break;
					}
					k++;
				}
			} else {
				chars[k] = c;
				if (c == '\\') {
					escaped = 1;
				} else {
					k++;
				}
			}
		}
		str = strncat (calloc ((pl->pos - start - shrink) + 1, 1), chars,
					   pl->pos - start - shrink);
	}
	if (long_string)
		pl->pos += 2;
	pl->pos++;
	return str;
}

static char *
PL_ParseUnquotedString (pldata_t *pl)
{
	unsigned int	start = pl->pos;
	char			*str;

	while (pl->pos < pl->end) {
		if (is_quotable (pl->ptr[pl->pos]))
			break;
		pl->pos++;
	}
	str = strncat (calloc ((pl->pos - start) + 1, 1), &pl->ptr[start],
				   pl->pos - start);
	return str;
}

static plitem_t *
PL_ParsePropertyListItem (pldata_t *pl)
{
	plitem_t	*item = NULL;

	if (!PL_SkipSpace (pl))
		return NULL;

	switch (pl->ptr[pl->pos]) {
	case '{':
	{
		item = PL_NewDictionary ();

		pl->pos++;

		while (PL_SkipSpace (pl) && pl->ptr[pl->pos] != '}') {
			plitem_t	*key;
			plitem_t	*value;

			if (!(key = PL_ParsePropertyListItem (pl))) {
				PL_Free (item);
				return NULL;
			}

			if (!(PL_SkipSpace (pl))) {
				PL_Free (key);
				PL_Free (item);
				return NULL;
			}

			if (key->type != QFString) {
				pl->error = "Key is not a string";
				PL_Free (key);
				PL_Free (item);
				return NULL;
			}

			if (pl->ptr[pl->pos] != '=') {
				pl->error = "Unexpected character (expected '=')";
				PL_Free (key);
				PL_Free (item);
				return NULL;
			}
			pl->pos++;

			// If there is no value, lose the key
			if (!(value = PL_ParsePropertyListItem (pl))) {
				PL_Free (key);
				PL_Free (item);
				return NULL;
			}

			if (!(PL_SkipSpace (pl))) {
				PL_Free (key);
				PL_Free (value);
				PL_Free (item);
				return NULL;
			}

			if (pl->ptr[pl->pos] == ';') {
				pl->pos++;
			} else if (pl->ptr[pl->pos] != '}') {
				pl->error = "Unexpected character (wanted ';' or '}')";
				PL_Free (key);
				PL_Free (value);
				PL_Free (item);
				return NULL;
			}

			// Add the key/value pair to the dictionary
			if (!PL_D_AddObject (item, PL_String (key), value)) {
				PL_Free (key);
				PL_Free (value);
				PL_Free (item);
				return NULL;
			}
			PL_Free (key);
		}

		if (pl->pos >= pl->end) {	// Catch the error
			pl->error = "Unexpected end of string when parsing dictionary";
			PL_Free (item);
			return NULL;
		}
		pl->pos++;

		return item;
	}

	case '(': {
		item = PL_NewArray ();

		pl->pos++;

		while (PL_SkipSpace (pl) && pl->ptr[pl->pos] != ')') {
			plitem_t	*value;

			if (!(value = PL_ParsePropertyListItem (pl))) {
				PL_Free (item);
				return NULL;
			}

			if (!(PL_SkipSpace (pl))) {
				PL_Free (value);
				PL_Free (item);
				return NULL;
			}

			if (pl->ptr[pl->pos] == ',') {
				pl->pos++;
			} else if (pl->ptr[pl->pos] != ')') {
				pl->error = "Unexpected character (wanted ',' or ')')";
				PL_Free (value);
				PL_Free (item);
				return NULL;
			}

			if (!PL_A_AddObject (item, value)) {
				pl->error = "Unexpected character (too many items in array)";
				PL_Free (value);
				PL_Free (item);
				return NULL;
			}
		}
		pl->pos++;

		return item;
	}

	case '<': {
		int len;
		char *str = PL_ParseData (pl, &len);

		if (!str) {
			return NULL;
		} else {
			return PL_NewData (str, len);
		}
	}

	case '"': {
		char *str = PL_ParseQuotedString (pl);

		if (!str) {
			return NULL;
		} else {
			return new_string (str);
		}
	}

	default: {
		char *str = PL_ParseUnquotedString (pl);

		if (!str) {
			return NULL;
		} else {
			return new_string (str);
		}
	}
	} // switch
}

VISIBLE plitem_t *
PL_GetPropertyList (const char *string)
{
	pldata_t	*pl = calloc (1, sizeof (pldata_t));
	plitem_t	*newpl = NULL;

	if (!quotable_bitmap[0])
		init_quotables ();

	pl->ptr = string;
	pl->pos = 0;
	pl->end = strlen (string);
	pl->error = NULL;
	pl->line = 1;

	if ((newpl = PL_ParsePropertyListItem (pl))) {
		free (pl);
		return newpl;
	} else {
		if (pl && pl->error && pl->error[0])
			Sys_Printf ("plist: %d,%d: %s", pl->line, pl->pos, pl->error);
		free (pl);
		return NULL;
	}
}

static void
write_tabs (dstring_t *dstr, int num)
{
	char       *tabs = dstring_reservestr (dstr, num);

	memset (tabs, '\t', num);
	tabs[num] = 0;
}

static void
write_string_len (dstring_t *dstr, const char *str, int len)
{
	char       *dst = dstring_reservestr (dstr, len);
	memcpy (dst, str, len);
	dst[len] = 0;
}

static char
to_hex (byte b)
{
	char        c = (b & 0xf) + '0';
	if (c > '9')
		c = c - '0' + 'A';
	return c;
}

static void
write_binary (dstring_t *dstr, byte *binary, int len)
{
	int         i;
	char       *dst = dstring_reservestr (dstr, len * 2);
	for (i = 0; i < len; i++) {
		*dst++ = to_hex (binary[i] >> 4);
		*dst++ = to_hex (binary[i]);
	}
}

static void
write_string (dstring_t *dstr, const char *str)
{
	const char *s;
	int         len = 0;
	char       *dst;
	int         quoted = 0;

	for (s = str; *s; s++) {
		if (is_quotable (*s))
			quoted = 1;
		len++;
	}
	if (!quoted) {
		dst = dstring_reservestr (dstr, len);
		strcpy (dst, str);
		return;
	}
	// assume worst case of all octal chars plus two quotes.
	dst = dstring_reservestr (dstr, len * 4 + 2);
	*dst++= '\"';
	while (*str) {
		if (*str && isascii ((byte) *str) && isprint ((byte) *str)
			&& *str != '\\' && *str != '\"') {
			*dst++ = *str++;
			continue;
		}
		if (*str) {
			*dst++ = '\\';
			switch (*str) {
				case '\"':
				case '\\':
					*dst++ = *str;
					break;
				case '\n':
					*dst++ = 'n';
					break;
				case '\a':
					*dst++ = 'a';
					break;
				case '\b':
					*dst++ = 'b';
					break;
				case '\f':
					*dst++ = 'f';
					break;
				case '\r':
					*dst++ = 'r';
					break;
				case '\t':
					*dst++ = 't';
					break;
				case '\v':
					*dst++ = 'v';
					break;
				default:
					*dst++ = '0' + ((((byte) *str) >> 6) & 3);
					*dst++ = '0' + ((((byte) *str) >> 3) & 7);
					*dst++ = '0' + (((byte) *str) & 7);
					break;
			}
			str++;
		}
	}
	*dst++ = '\"';
	*dst++ = 0;
	dstr->size = dst - dstr->str;
}

static void
write_item (dstring_t *dstr, plitem_t *item, int level)
{
	void		**list, **l;
	dictkey_t	*current;
	plarray_t	*array;
	plbinary_t	*binary;
	int			i;

	switch (item->type) {
		case QFDictionary:
			write_string_len (dstr, "{\n", 2);
			l = list = Hash_GetList ((hashtab_t *) item->data);
			while ((current = (dictkey_t *) *l++)) {
				write_tabs (dstr, level + 1);
				write_string (dstr, current->key);
				write_string_len (dstr, " = ", 3);
				write_item (dstr, current->value, level + 1);
				write_string_len (dstr, ";\n", 2);
			}
			free (list);
			write_tabs (dstr, level);
			write_string_len (dstr, "}", 1);
			break;
		case QFArray:
			write_string_len (dstr, "(\n", 2);
			array = (plarray_t *) item->data;
			for (i = 0; i < array->numvals; i++) {
				write_tabs (dstr, level + 1);
				write_item (dstr, array->values[i], level + 1);
				if (i < array->numvals - 1)
					write_string_len (dstr, ",\n", 2);
			}
			write_string_len (dstr, "\n", 1);
			write_tabs (dstr, level);
			write_string_len (dstr, ")", 1);
			break;
		case QFBinary:
			write_string_len (dstr, "<", 1);
			binary = (plbinary_t *) item->data;
			write_binary (dstr, binary->data, binary->size);
			write_string_len (dstr, ">", 1);
			break;
		case QFString:
			write_string (dstr, item->data);
			break;
		default:
			break;
	}
}

VISIBLE char *
PL_WritePropertyList (plitem_t *pl)
{
	dstring_t  *dstr = dstring_newstr ();

	if (!quotable_bitmap[0])
		init_quotables ();
	write_item (dstr, pl, 0);
	write_string_len (dstr, "\n", 1);
	return dstring_freeze (dstr);
}

VISIBLE pltype_t
PL_Type (plitem_t *item)
{
	return item->type;
}
