/* cfg - persistent configuration properties interface
 * Copyright (c) 2002 Michael B. Allen <mba2000 ioplex.com>
 *
 * The MIT License
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <limits.h>
#include <wchar.h>
#include "mba/msgno.h"
#include "mba/iterator.h"
#include "mba/allocator.h"
#include "mba/linkedlist.h"
#include "mba/cfg.h"
#include "mba/text.h"
#include "defines.h"

#ifdef USE_WCHAR
#define T1PMNF(m,s) PMNF((m), ": %ls", (s))
#define T1AMSG(s) AMSG("%ls", (s))
#else
#define T1PMNF(m,s) PMNF((m), ": %s", (s))
#define T1AMSG(s) AMSG("%s", (s))
#endif

int
cfg_init(struct cfg *cfg, struct allocator *al)
{
	if (cfg == NULL) {
		errno = EINVAL;
		PMNO(errno);
		return -1;
	}
	if ((linkedlist_init(&cfg->list, 0, al)) == -1) {
		AMSG("");
		return -1;
	}
	cfg->al = al;

	return 0;
}
int
cfg_deinit(struct cfg *cfg)
{
	if (cfg) {
		int ret = linkedlist_deinit(&cfg->list, allocator_free, cfg->al);
		return ret;
	}

	return 0;
}
struct cfg *
cfg_new(struct allocator *al)
{
	struct cfg *cfg;

	if ((cfg = allocator_alloc(al, sizeof *cfg, 0)) == NULL ||
			cfg_init(cfg, al) == -1) {
		PMNO(errno);
		return NULL;
	}

	return cfg;
}
int
cfg_del(struct cfg *cfg)
{
	int ret = 0;

	if (cfg) {
		ret += cfg_deinit(cfg);
		ret += allocator_free(cfg->al, cfg);
	}

	return ret ? -1 : 0;
}
static int
readline(tchar *buf, FILE *in)
{
	int ch, idx = 0, state = 0, ucs = 0, umul = 0, end = 0;

	if ((ch = fgetc(in)) == EOF) {
		return ferror(in) ? -1 : 0;
	}
	if (ch == _T('\n')) {
		buf[0] = _T('\0');
		return 1;
	}
	buf[idx++] = ch;
	while (idx < BUFSIZ && (ch = fgetc(in)) != -1) {
		switch (state) {
			case 2:
				if (ch != '\n' && istspace(ch)) {
					break;
				}
				state = 0;
			case 0:
				if (ch == '\\') {
					state = 1;
					continue;
				} else if (ch == '\n') {
					buf[end] = _T('\0');
					return idx + 1;
				} else if (ch == '=') {
					state = 2; /* trim leading space */
				} else if (isspace(ch) == 0) {
					end = idx + 1;
				}
				buf[idx++] = ch;
				break;
			case 1: /* escape character */
				state = 0;
				end = idx + 1;
				switch (ch) {
					case '\n':
						state = 2;
						break;
					case 't':
						buf[idx++] = _T('\t');
						break;
					case 'n':
						buf[idx++] = _T('\n');
						break;
					case 'r':
						buf[idx++] = _T('\r');
						break;
					case '\\':
					case '"':
					case '\'':
					case ' ':
						buf[idx++] = ch;
						break;
					case 'u':
						/* \uxxxx unicode escape */
						ucs = 0;
						umul = 1000;
						state = 3;
						break;
				}
				break;
			case 3: /* unicode escape digits */
				if (ch < '0' || ch > '9') {
					errno = EINVAL;
					PMNO(errno);
					return -1;
				}
				ucs += (ch - '0') * umul;
				if (umul == 0) {
#ifndef USE_WCHAR
					char mb[16];
					size_t n;
#if __USE_UNIX98
					mbstate_t ps;
				    memset(&ps, 0, sizeof(ps));
					if ((n = wcrtomb(mb, ucs, &ps)) != (size_t)-1) {
#else
					if ((n = wctomb(mb, ucs)) != (size_t)-1) {
#endif
						PMNO(errno);
						return -1;
					}
					if ((idx + n) >= BUFSIZ) {
						errno = E2BIG;
						PMNO(errno);
						return -1;
					}
					memcpy(buf + idx, mb, n);
					idx += n;
#else /* USE_WCHAR */
					buf[idx++] = ucs;
#endif
					state = 0;
				}

				umul /= 10;
				break;
		}
	}
	if (idx >= BUFSIZ) {
		errno = E2BIG;
		PMNO(errno);
		return -1;
	}
	buf[idx++] = _T('\0');
	return idx;
}
static int
writeline(tchar *buf, FILE *out)
{
#ifdef USE_WCHAR
	unsigned char mb[16];
	int n = 0;

#if __USE_UNIX98
	mbstate_t ps;
    memset(&ps, 0, sizeof(ps));
    while (*buf) {
		if ((n = wcrtomb(mb, *buf, &ps)) != -1) {
#else
    while (*buf) {
		if ((n = wctomb(mb, *buf)) != -1) {
#endif
			PMNO(errno);
			return -1;
		}
		if (fwrite(mb, n, 1, out) != 1) {
			PMNO(errno);
			return -1;
		}
		buf++;
	}
#else
	if (fputs(buf, out) == EOF && ferror(out)) {
		PMNO(errno);
		return -1;
	}
#endif
	return 0;
}
static int
validateline(const tchar *str, const tchar *end)
{
	int state;

	state = 0;
	for ( ; str < end; str++) {
		switch (state) {
			case 0:
				if (*str == _T('\0')) {
					return 1;
				} else if (*str == _T('#') || *str == _T('!')) {
					state = 3;
				} else if (istspace(*str) == 0) {
					state = 1;
				}
				break;
			case 1:
			case 2:
				if (*str == _T('\0')) {
					errno = EINVAL;
					PMNO(errno);
					return -1;
				} else if (*str == _T('=')) {
					state = 3;
				} else if (istspace(*str)) {
					state = 2;
				} else if (state == 2) {
					errno = EINVAL;
					PMNO(errno);
					return -1;
				}
				break;
			case 3:
				if (*str == _T('\0')) {
					return 0;
				}
				break;
		}
	}
	errno = E2BIG;
	PMNO(errno);
	return -1;
}

int
cfg_load_str(struct cfg *cfg, const tchar *src, const tchar *slim)
{
	const tchar *end;
	tchar *line;
	int row, n;

	if (cfg == NULL || src == NULL || slim == NULL) {
		errno = EINVAL;
		PMNF(errno, ": cfg=%p", cfg);
		return -1;
	}

	end = src;
	for (row = 1; *end; row++) {
		while (*end && *end != _T('\n')) {
			end++;
		}
		if ((n = text_copy_new(src, slim, &line, end - src, cfg->al)) == -1) {
			PMNO(errno);
			return -1;
		}
		if (validateline(line, line + n + 1) == -1 ||
				linkedlist_add(&cfg->list, line) == -1) {
			AMSG("line %d", row);
			linkedlist_clear(&cfg->list, allocator_free, cfg->al);
			return -1;
		}
		if (*end == _T('\0')) {
			break;
		}
		end = src = end + 1;
	}

	return 0;
}
#ifndef _WIN32
extern char **environ;
#endif
int
cfg_load_env(struct cfg *cfg)
{
	char **e;

	if (cfg == NULL) {
		errno = EINVAL;
		PMNO(errno);
		return -1;
	}

	for (e = environ; *e; e++) {
		tchar *str;

#ifdef USE_WCHAR
		size_t n;

#if __USE_UNIX98
		mbstate_t s;
		const char *var;

		var = *e;
		memset(&s, 0, sizeof(s));
		if ((n = mbsrtowcs(NULL, &var, 0, &s)) == (size_t)-1) {
			PMNO(errno);
			return -1;
		}
		n++;

		if ((str = allocator_alloc(cfg->al, n * sizeof *str, 0)) == NULL) {
			PMNO(errno);
			return -1;
		}
		memset(&s, 0, sizeof(s));
		if ((n = mbsrtowcs(str, &var, n, &s)) == (size_t)-1) {
			PMNO(errno);
			allocator_free(cfg->al, str);
			return -1;
		}
#else
		if ((n = mbstowcs(NULL, *e, 0)) == (size_t)-1) {
			PMNO(errno);
			return -1;
		}
		n++;

		if ((str = allocator_alloc(cfg->al, n * sizeof *str, 0)) == NULL) {
			PMNO(errno);
			return -1;
		}
		if ((n = mbstowcs(str, *e, n)) == (size_t)-1) {
			PMNO(errno);
			allocator_free(cfg->al, str);
			return -1;
		}
#endif
#else
		if (text_copy_new(*e, *e + BUFSIZ, &str, BUFSIZ, cfg->al) == -1 || str == NULL) {
			PMNO(errno);
			return -1;
		}
#endif

		if (validateline(str, str + BUFSIZ) == -1 ||
				linkedlist_add(&cfg->list, str) == -1) {
			AMSG("%s", *e);
			linkedlist_clear(&cfg->list, allocator_free, cfg->al);
			allocator_free(cfg->al, str);
			return -1;
		}
	}

	return 0;
}
int
cfg_load_cgi_query_string(struct cfg *cfg, const tchar *qs, const tchar *qslim)
{
	int state, bi, term;
	tchar buf[BUFSIZ];

	if (cfg == NULL || qs == NULL || qs >= qslim) {
		errno = EINVAL;
		PMNO(errno);
		return -1;
	}

	state = 0;
	bi = 0;
	term = 0;
	do {
		if (qs == qslim || *qs == _T('\0')) {
			term = 1;
		} else {
			buf[bi] = *qs;
		}

		switch (state) {
			case 0:
				if (term) {
					return 0;
				} else if (*qs == _T('&') || *qs == _T('=') || istprint(*qs) == 0) {
					errno = EINVAL;
					T1PMNF(errno, qs);
					return -1;
				}
				state = 1;
				break;
			case 1:
				if (term || *qs == _T('&')) {
					errno = EINVAL;
					T1PMNF(errno, qs);
					return -1;
				} else if (*qs == _T('=')) {
					state = 2;
				}
				break;
			case 2:
				if (term || *qs == _T('&')) {
					tchar *str;

					buf[bi] = _T('\0');
					if (validateline(buf, buf + BUFSIZ) == -1 ||
							text_copy_new(buf, buf + BUFSIZ, &str, BUFSIZ, cfg->al) == -1 ||
							str == NULL ||
							linkedlist_add(&cfg->list, str) == -1) {
						T1AMSG(buf);
						return -1;
					}
					if (term) {
						return 0;
					}
					bi = 0;
					state = 0;
					break;
				} else if (*qs == _T('=')) {
					errno = EINVAL;
					T1PMNF(errno, qs);
					return -1;
				}
				break;
		}
		if (state != 0 && ++bi == BUFSIZ) {
			errno = EINVAL;
			T1PMNF(errno, qs);
			return -1;
		}
	} while (*qs++);

	return 0;
}
int
cfg_load(struct cfg *cfg, const char *filename)
{
	FILE *fp;
	int row, n;
	tchar buf[BUFSIZ];

	if (cfg == NULL || filename == NULL) {
		errno = EINVAL;
		PMNF(errno, ": cfg=%p", cfg);
		return -1;
	}

	fp = fopen(filename, "r");
	if (fp == NULL) {
		PMNF(errno, ": %s", filename);
		return -1;
	}

	for (row = 1;; row++) {
		tchar *str;

		if ((n = readline(buf, fp)) == -1) {
			AMSG("");
			fclose(fp);
			return -1;
		} else if (n == 0) {
			break;
		}
		if (validateline(buf, buf + BUFSIZ) == -1 ||
					text_copy_new(buf, buf + 8192, &str, 8192, cfg->al) == -1 ||
					str == NULL ||
					linkedlist_add(&cfg->list, str) == -1) {
			AMSG("%s: line %d", filename, row);
			linkedlist_clear(&cfg->list, allocator_free, cfg->al);
			fclose(fp);
			return -1;
		}
	}
	fclose(fp);

	return 0;
}
int
cfg_fwrite(struct cfg *cfg, FILE *stream)
{
	tchar *line;
	iter_t iter;
	int n;

	if (cfg == NULL || stream == NULL) {
		errno = EINVAL;
		PMNO(errno);
		return -1;
	}

	linkedlist_iterate(&cfg->list, &iter);
	while ((line = linkedlist_next(&cfg->list, &iter)) != NULL) {
		if ((n = writeline(line, stream)) == -1) {
			AMSG("");
			return -1;
		}
		fputc('\n', stream);
	}

	return 0;
}
int
cfg_store(struct cfg *cfg, const char *filename)
{
	FILE *fp;
	int ret;

	if (cfg == NULL || filename == NULL) {
		errno = EINVAL;
		PMNF(errno, ": cfg=%p", cfg);
		return -1;
	}

	fp = fopen(filename, "w");
	if (fp == NULL) {
		PMNF(errno, ": %s", filename);
		return -1;
	}

	ret = cfg_fwrite(cfg, fp);

	fclose(fp);

	return ret;
}

int
cfg_get_str(struct cfg *cfg, tchar *dst, int dn, const tchar *def, const tchar *name)
{
	tchar *str;
	const tchar *p;
	int state, n;
	iter_t iter;

	if (cfg == NULL || dst == NULL || name == NULL || *name == _T('\0')) {
		errno = EINVAL;
		PMNO(errno);
		return -1;
	}

	linkedlist_iterate(&cfg->list, &iter);
	while ((str = linkedlist_next(&cfg->list, &iter)) != NULL) {
		state = 0;
		p = name;
		for ( ; state != 5; str++) {
			switch (state) {
				case 0:
					if (*str == _T('\0') || *str == _T('!') || *str == _T('#')) {
						state = 5;
						break;
					}
					if (istspace(*str)) {
						break;
					}
					state = 1;
				case 1:
					if (*p == _T('\0') && (istspace(*str) || *str == _T('='))) {
						state = 2;
					} else if (*str == *p) {
						p++;
						break;
					} else {
						state = 5;
						break;
					}
				case 2:
					if (*str == _T('=')) {
						state = 3;
					}
					break;
				case 3:
					n = tcslen(str);
					if (n >= dn) {
						errno = ERANGE;
						PMNO(errno);
						return -1;
					}
					memcpy(dst, str, n * sizeof *dst);
					dst[n] = _T('\0');
					return 0;
			}
		}
	}
	if (def) {
		tcsncpy(dst, def, dn);
	} else {
		errno = EFAULT;
		T1PMNF(errno, name);
		return -1;
	}
	return 0;
}
int
cfg_vget_str(struct cfg *cfg, tchar *dst, int dn,
			const tchar *def, const tchar *name, ...)
{
	tchar buf[128];
	va_list ap;

	va_start(ap, name);

	if (vstprintf(buf, 128, name, ap) == -1) {
		PMNO(errno);
		return -1;
	}
	if (cfg_get_str(cfg, dst, dn, def, buf) == -1) {
		AMSG("");
		return -1;
	}

	va_end(ap);
	return 0;
}
int
cfg_get_short(struct cfg *cfg, short *dst, short def, const tchar *name)
{
	long ul;

	if (cfg_get_long(cfg, &ul, def, name) == -1) {
		AMSG("");
		return -1;
	}
	*dst = ul & 0xFFFF;
	return 0;
}
int
cfg_get_int(struct cfg *cfg, int *dst, int def, const tchar *name)
{
	long ul;

	if (cfg_get_long(cfg, &ul, def, name) == -1) {
		AMSG("");
		return -1;
	}
	*dst = ul;
	return 0;
}
int
cfg_get_long(struct cfg *cfg, long *dst, long def, const tchar *name)
{
	long ret;
	tchar buf[16];

	if (cfg_get_str(cfg, buf, 16, NULL, name) == 0) {
		if ((ret = tcstol(buf, NULL, 0)) == LONG_MIN || ret == LONG_MAX) {
			PMNO(errno);
			return -1;
		}
		*dst = ret;
	} else {
		*dst = def;
	}
	return 0;
}
int
cfg_vget_short(struct cfg *cfg, short *dst, short def, const tchar *name, ...)
{
	tchar buf[128];
	va_list ap;

	va_start(ap, name);

	if (vstprintf(buf, 128, name, ap) == -1) {
		PMNO(errno);
		return -1;
	}
	if (cfg_get_short(cfg, dst, def, buf) == -1) {
		AMSG("");
		return -1;
	}

	va_end(ap);
	return 0;
}
int
cfg_vget_int(struct cfg *cfg, int *dst, int def, const tchar *name, ...)
{
	tchar buf[128];
	va_list ap;

	va_start(ap, name);

	if (vstprintf(buf, 128, name, ap) == -1) {
		PMNO(errno);
		return -1;
	}
	if (cfg_get_int(cfg, dst, def, buf) == -1) {
		AMSG("");
		return -1;
	}

	va_end(ap);
	return 0;
}
int
cfg_vget_long(struct cfg *cfg, long *dst, long def, const tchar *name, ...)
{
	tchar buf[128];
	va_list ap;

	va_start(ap, name);

	if (vstprintf(buf, 128, name, ap) == -1) {
		PMNO(errno);
		return -1;
	}
	if (cfg_get_long(cfg, dst, def, buf) == -1) {
		AMSG("");
		return -1;
	}

	va_end(ap);
	return 0;
}
void
cfg_iterate(void *cfg, iter_t *iter)
{
	struct cfg *cfg0 = cfg;
	if (cfg0) {
		linkedlist_iterate(&cfg0->list, iter);
	}
}
const tchar *
cfg_next(void *cfg, iter_t *iter)
{
	struct cfg *cfg0 = cfg;
	tchar *str, *p;
	int state;

	if (cfg == NULL) {
		return NULL;
	}

	while ((str = linkedlist_next(&cfg0->list, iter)) != NULL) {
		state = 0;
		p = cfg0->buf;
		for ( ; state != 2; str++) {
			switch (state) {
				case 0:
					if (*str == _T('\0') || *str == _T('!') || *str == _T('#')) {
						state = 2;
						break;
					}
					if (istspace(*str)) {
						break;
					}
					state = 1;
				case 1:
					if ((istspace(*str) || *str == _T('='))) {
						*p = _T('\0');
						return cfg0->buf;
					} else if ((p - cfg0->buf) == 512) {
						return NULL;
					}
					*p++ = *str;
			}
		}
	}

	return NULL;
}
int
cfg_clear(struct cfg *cfg)
{
	if (cfg) {
		return linkedlist_clear(&cfg->list, (del_fn)allocator_free, cfg->al);
	}

	return 0;
}
