/*
 * Copyright (C) 2002 Roman Zippel <zippel@linux-m68k.org>
 * Released under the terms of the GNU GPL v2.0.
 */

#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <sys/utsname.h>

#define LKC_DIRECT_LINK
#include "lkc.h"

struct symbol symbol_yes = {
	name: "y",
	curr: { "y", yes },
	flags: SYMBOL_YES|SYMBOL_VALID,
}, symbol_mod = {
	name: "m",
	curr: { "m", mod },
	flags: SYMBOL_MOD|SYMBOL_VALID,
}, symbol_no = {
	name: "n",
	curr: { "n", no },
	flags: SYMBOL_NO|SYMBOL_VALID,
}, symbol_empty = {
	name: "",
	curr: { "", no },
	flags: SYMBOL_VALID,
};

int sym_change_count;
struct symbol *modules_sym;

void sym_add_default(struct symbol *sym, const char *def)
{
	struct property *prop = create_prop(P_DEFAULT);
	struct property **propp;

	prop->sym = sym;
	prop->def = sym_lookup(def, 1);

	/* append property to the prop list of symbol */
	if (prop->sym) {
		for (propp = &prop->sym->prop; *propp; propp = &(*propp)->next)
			;
		*propp = prop;
	}
}

void sym_init(void)
{
	struct symbol *sym;
	struct utsname uts;
	char *p;
	static bool inited = false;

	if (inited)
		return;
	inited = true;

	uname(&uts);

#if 0
	sym = sym_lookup("ARCH", 0);
	sym->type = S_STRING;
	sym->flags |= SYMBOL_AUTO;
	p = getenv("ARCH");
	if (p)
		sym_add_default(sym, p);
#endif

	sym = sym_lookup("VERSION", 0);
	sym->type = S_STRING;
	sym->flags |= SYMBOL_AUTO;
	p = getenv("VERSION");
	if (p)
		sym_add_default(sym, p);

#if 0
	sym = sym_lookup("UNAME_RELEASE", 0);
	sym->type = S_STRING;
	sym->flags |= SYMBOL_AUTO;
	sym_add_default(sym, uts.release);
#endif

	sym = sym_lookup("TARGET_ARCH", 0);
	sym->type = S_STRING;
	sym->flags |= SYMBOL_AUTO;
	p = getenv("TARGET_ARCH");
	if (p)
		sym_add_default(sym, p);
}

int sym_get_type(struct symbol *sym)
{
	int type = sym->type;
	if (type == S_TRISTATE) {
		if (sym_is_choice_value(sym) && sym->visible == yes)
			type = S_BOOLEAN;
		else {
			sym_calc_value(modules_sym);
			if (S_TRI(modules_sym->curr) == no)
				type = S_BOOLEAN;
		}
	}
	return type;
}

const char *sym_type_name(int type)
{
	switch (type) {
	case S_BOOLEAN:
		return "boolean";
	case S_TRISTATE:
		return "tristate";
	case S_INT:
		return "integer";
	case S_HEX:
		return "hex";
	case S_STRING:
		return "string";
	case S_UNKNOWN:
		return "unknown";
	}
	return "???";
}

struct property *sym_get_choice_prop(struct symbol *sym)
{
	struct property *prop;

	for_all_choices(sym, prop)
		return prop;
	return NULL;
}

struct property *sym_get_default_prop(struct symbol *sym)
{
	struct property *prop;
	tristate visible;

	for_all_defaults(sym, prop) {
		visible = E_CALC(prop->visible);
		if (visible != no)
			return prop;
	}
	return NULL;
}

void sym_calc_visibility(struct symbol *sym)
{
	struct property *prop;
	tristate visible, oldvisible;

	/* any prompt visible? */
	oldvisible = sym->visible;
	visible = no;
	for_all_prompts(sym, prop)
		visible = E_OR(visible, E_CALC(prop->visible));
	if (oldvisible != visible) {
		sym->visible = visible;
		sym->flags |= SYMBOL_CHANGED;
	}
}

void sym_calc_value(struct symbol *sym)
{
	struct symbol_value newval, oldval;
	struct property *prop, *def_prop;
	struct symbol *def_sym;
	struct expr *e;

	if (sym->flags & SYMBOL_VALID)
		return;

	oldval = sym->curr;

	switch (sym->type) {
	case S_INT:
	case S_HEX:
	case S_STRING:
		newval = symbol_empty.curr;
		break;
	case S_BOOLEAN:
	case S_TRISTATE:
		newval = symbol_no.curr;
		break;
	default:
		S_VAL(newval) = sym->name;
		S_TRI(newval) = no;
		if (sym->flags & SYMBOL_CONST) {
			goto out;
		}
		//newval = symbol_empty.curr;
		// generate warning somewhere here later
		//S_TRI(newval) = yes;
		goto out;
	}
	sym->flags |= SYMBOL_VALID;
	if (!sym_is_choice_value(sym))
		sym->flags &= ~SYMBOL_WRITE;

	sym_calc_visibility(sym);

	/* set default if recursively called */
	sym->curr = newval;

	if (sym->visible != no) {
		sym->flags |= SYMBOL_WRITE;
		if (!sym_has_value(sym)) {
			if (!sym_is_choice(sym)) {
				prop = sym_get_default_prop(sym);
				if (prop) {
					sym_calc_value(prop->def);
					newval = prop->def->curr;
				}
			}
		} else
			newval = sym->def;

		S_TRI(newval) = E_AND(S_TRI(newval), sym->visible);
		/* if the symbol is visible and not optionial,
		 * possibly ignore old user choice. */
		if (!sym_is_optional(sym) && S_TRI(newval) == no)
			S_TRI(newval) = sym->visible;
		if (sym_is_choice_value(sym) && sym->visible == yes) {
			prop = sym_get_choice_prop(sym);
			S_TRI(newval) = (S_VAL(prop->def->curr) == sym) ? yes : no;
		}
	} else {
		prop = sym_get_default_prop(sym);
		if (prop) {
			sym->flags |= SYMBOL_WRITE;
			sym_calc_value(prop->def);
			newval = prop->def->curr;
		}
	}

	switch (sym_get_type(sym)) {
	case S_TRISTATE:
		if (S_TRI(newval) != mod)
			break;
		sym_calc_value(modules_sym);
		if (S_TRI(modules_sym->curr) == no)
			S_TRI(newval) = yes;
		break;
	case S_BOOLEAN:
		if (S_TRI(newval) == mod)
			S_TRI(newval) = yes;
	}

out:
	sym->curr = newval;

	if (sym_is_choice(sym) && S_TRI(newval) == yes) {
		def_sym = S_VAL(sym->def);
		if (def_sym) {
			sym_calc_visibility(def_sym);
			if (def_sym->visible == no)
				def_sym = NULL;
		}
		if (!def_sym) {
			for_all_defaults(sym, def_prop) {
				if (E_CALC(def_prop->visible) == no)
					continue;
				sym_calc_visibility(def_prop->def);
				if (def_prop->def->visible != no) {
					def_sym = def_prop->def;
					break;
				}
			}
		}

		if (!def_sym) {
			prop = sym_get_choice_prop(sym);
			for (e = prop->dep; e; e = e->left.expr) {
				sym_calc_visibility(e->right.sym);
				if (e->right.sym->visible != no) {
					def_sym = e->right.sym;
					break;
				}
			}
		}

		S_VAL(newval) = def_sym;
	}

	if (memcmp(&oldval, &newval, sizeof(newval)))
		sym->flags |= SYMBOL_CHANGED;
	sym->curr = newval;

	if (sym_is_choice(sym)) {
		int flags = sym->flags & (SYMBOL_CHANGED | SYMBOL_WRITE);
		prop = sym_get_choice_prop(sym);
		for (e = prop->dep; e; e = e->left.expr)
			e->right.sym->flags |= flags;
	}
}

void sym_clear_all_valid(void)
{
	struct symbol *sym;
	int i;

	for_all_symbols(i, sym)
		sym->flags &= ~SYMBOL_VALID;
	sym_change_count++;
}

void sym_set_all_changed(void)
{
	struct symbol *sym;
	int i;

	for_all_symbols(i, sym)
		sym->flags |= SYMBOL_CHANGED;
}

bool sym_tristate_within_range(struct symbol *sym, tristate val)
{
	int type = sym_get_type(sym);

	if (sym->visible == no)
		return false;

	if (type != S_BOOLEAN && type != S_TRISTATE)
		return false;

	switch (val) {
	case no:
		if (sym_is_choice_value(sym) && sym->visible == yes)
			return false;
		return sym_is_optional(sym);
	case mod:
		if (sym_is_choice_value(sym) && sym->visible == yes)
			return false;
		return type == S_TRISTATE;
	case yes:
		return type == S_BOOLEAN || sym->visible == yes;
	}
	return false;
}

bool sym_set_tristate_value(struct symbol *sym, tristate val)
{
	tristate oldval = sym_get_tristate_value(sym);

	if (oldval != val && !sym_tristate_within_range(sym, val))
		return false;

	if (sym->flags & SYMBOL_NEW) {
		sym->flags &= ~SYMBOL_NEW;
		sym->flags |= SYMBOL_CHANGED;
	}
	if (sym_is_choice_value(sym) && val == yes) {
		struct property *prop = sym_get_choice_prop(sym);

		S_VAL(prop->def->def) = sym;
		prop->def->flags &= ~SYMBOL_NEW;
	}

	S_TRI(sym->def) = val;
	if (oldval != val) {
		sym_clear_all_valid();
		if (sym == modules_sym)
			sym_set_all_changed();
	}

	return true;
}

tristate sym_toggle_tristate_value(struct symbol *sym)
{
	tristate oldval, newval;

	oldval = newval = sym_get_tristate_value(sym);
	do {
		switch (newval) {
		case no:
			newval = mod;
			break;
		case mod:
			newval = yes;
			break;
		case yes:
			newval = no;
			break;
		}
		if (sym_set_tristate_value(sym, newval))
			break;
	} while (oldval != newval);
	return newval;
}

bool sym_string_valid(struct symbol *sym, const char *str)
{
	char ch;

	switch (sym->type) {
	case S_STRING:
		return true;
	case S_INT:
		ch = *str++;
		if (ch == '-')
			ch = *str++;
		if (!isdigit((int)ch))
			return false;
		if (ch == '0' && *str != 0)
			return false;
		while ((ch = *str++)) {
			if (!isdigit((int)ch))
				return false;
		}
		return true;
	case S_HEX:
		if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X'))
			str += 2;
		ch = *str++;
		do {
			if (!isxdigit((int)ch))
				return false;
		} while ((ch = *str++));
		return true;
	case S_BOOLEAN:
	case S_TRISTATE:
		switch (str[0]) {
		case 'y':
		case 'Y':
			return sym_tristate_within_range(sym, yes);
		case 'm':
		case 'M':
			return sym_tristate_within_range(sym, mod);
		case 'n':
		case 'N':
			return sym_tristate_within_range(sym, no);
		}
		return false;
	default:
		return false;
	}
}

bool sym_set_string_value(struct symbol *sym, const char *newval)
{
	const char *oldval;
	char *val;
	int size;

	switch (sym->type) {
	case S_BOOLEAN:
	case S_TRISTATE:
		switch (newval[0]) {
		case 'y':
		case 'Y':
			return sym_set_tristate_value(sym, yes);
		case 'm':
		case 'M':
			return sym_set_tristate_value(sym, mod);
		case 'n':
		case 'N':
			return sym_set_tristate_value(sym, no);
		}
		return false;
	default:
		;
	}

	if (!sym_string_valid(sym, newval))
		return false;

	if (sym->flags & SYMBOL_NEW) {
		sym->flags &= ~SYMBOL_NEW;
		sym->flags |= SYMBOL_CHANGED;
	}

	oldval = S_VAL(sym->def);
	size = strlen(newval) + 1;
	if (sym->type == S_HEX && (newval[0] != '0' || (newval[1] != 'x' && newval[1] != 'X'))) {
		size += 2;
		S_VAL(sym->def) = val = malloc(size);
		*val++ = '0';
		*val++ = 'x';
	} else if (!oldval || strcmp(oldval, newval))
		S_VAL(sym->def) = val = malloc(size);
	else
		return true;

	strcpy(val, newval);
	free((void *)oldval);
	sym_clear_all_valid();

	return true;
}

const char *sym_get_string_value(struct symbol *sym)
{
	tristate val;

	switch (sym->type) {
	case S_BOOLEAN:
	case S_TRISTATE:
		val = sym_get_tristate_value(sym);
		switch (val) {
		case no:
			return "n";
		case mod:
			return "m";
		case yes:
			return "y";
		}
		break;
	default:
		;
	}
	return (const char *)S_VAL(sym->curr);
}

bool sym_is_changable(struct symbol *sym)
{
	if (sym->visible == no)
		return false;
	/* at least 'n' and 'y'/'m' is selectable */
	if (sym_is_optional(sym))
		return true;
	/* no 'n', so 'y' and 'm' must be selectable */
	if (sym_get_type(sym) == S_TRISTATE && sym->visible == yes)
		return true;
	return false;
}

struct symbol *sym_lookup(const char *name, int isconst)
{
	struct symbol *symbol;
	const char *ptr;
	char *new_name;
	int hash = 0;

	//printf("lookup: %s -> ", name);
	if (name) {
		if (name[0] && !name[1]) {
			switch (name[0]) {
			case 'y': return &symbol_yes;
			case 'm': return &symbol_mod;
			case 'n': return &symbol_no;
			}
		}
		for (ptr = name; *ptr; ptr++)
			hash += *ptr;
		hash &= 0xff;

		for (symbol = symbol_hash[hash]; symbol; symbol = symbol->next) {
			if (!strcmp(symbol->name, name)) {
				if ((isconst && symbol->flags & SYMBOL_CONST) ||
				    (!isconst && !(symbol->flags & SYMBOL_CONST))) {
					//printf("h:%p\n", symbol);
					return symbol;
				}
			}
		}
		new_name = strdup(name);
	} else {
		new_name = NULL;
		hash = 256;
	}

	symbol = malloc(sizeof(*symbol));
	memset(symbol, 0, sizeof(*symbol));
	symbol->name = new_name;
	symbol->type = S_UNKNOWN;
	symbol->flags = SYMBOL_NEW;
	if (isconst)
		symbol->flags |= SYMBOL_CONST;

	symbol->next = symbol_hash[hash];
	symbol_hash[hash] = symbol;

	//printf("n:%p\n", symbol);
	return symbol;
}

struct symbol *sym_find(const char *name)
{
	struct symbol *symbol = NULL;
	const char *ptr;
	int hash = 0;

	if (!name)
		return NULL;

	if (name[0] && !name[1]) {
		switch (name[0]) {
		case 'y': return &symbol_yes;
		case 'm': return &symbol_mod;
		case 'n': return &symbol_no;
		}
	}
	for (ptr = name; *ptr; ptr++)
		hash += *ptr;
	hash &= 0xff;

	for (symbol = symbol_hash[hash]; symbol; symbol = symbol->next) {
		if (!strcmp(symbol->name, name) &&
		    !(symbol->flags & SYMBOL_CONST))
				break;
	}

	return symbol;
}

const char *prop_get_type_name(enum prop_type type)
{
	switch (type) {
	case P_PROMPT:
		return "prompt";
	case P_COMMENT:
		return "comment";
	case P_MENU:
		return "menu";
	case P_ROOTMENU:
		return "rootmenu";
	case P_DEFAULT:
		return "default";
	case P_CHOICE:
		return "choice";
	default:
		return "unknown";
	}
}