/*
** SellaNMS
** $Id: configuration_parser.cxx,v 1.2 2006/01/24 07:08:09 sella Exp $
** Copyright (c) 2001-2005 Digital Genesis Software, LLC. All Rights Reserved.
** Released under the GPL Version 2 License.
** http://www.digitalgenesis.com
*/

static const char rcsid[] = "$Id: configuration_parser.cxx,v 1.2 2006/01/24 07:08:09 sella Exp $";

#include "configuration_parser.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>

#define BUF_SIZE 256

ConfigurationParser::ConfigurationParser(void) {
}

RESULT ConfigurationParser::loadConfiguration(slist<string> &errors, ConfigNode &configuration, string configuration_file) {
	RESULT result = SUCCESS;

	stack<string>	words;
	stack<config_token>  tokens;

	if (tokenizeConfiguration(errors, tokens, words, configuration_file) != SUCCESS) {
		result = FAILURE;
	} else if (convertToConfigNode(errors, configuration, tokens, words) != SUCCESS) {
		result = FAILURE;
	}
	errors.unique();

	return result;
}

RESULT ConfigurationParser::convertToConfigNode(slist<string> &errors, ConfigNode &root, stack<config_token> &tokens, stack<string> &words) {
	RESULT result = SUCCESS;

	ConfigNode node[NODE_ARRAY_SIZE];
	const ConfigNode empty_node;

	static const unsigned char next_state[14][7] = {
//		   E   W   ;   [   ]   {   }
		{ 11, 12,  1, 12, 12, 12,  2 },	//  0
		{ 11,  4,  1, 12,  3,  7,  2 },	//  1
		{ 12, 12,  1, 12, 12,  7,  2 },	//  2
		{ 12,  5, 12,  6, 12, 12, 12 },	//  3
		{ 11,  4,  1, 12, 12,  7,  2 },	//  4
		{ 12,  5, 12,  6, 12, 12, 12 },	//  5
		{ 12,  4, 12, 12, 12, 12, 12 },	//  6
		{ 12,  8, 12, 12, 12, 12, 12 },	//  7
		{ 11,  8,  9, 12, 12, 13, 10 },	//  8
		{ 11,  4,  1, 12,  3,  7,  2 },	//  9
		{ 12, 12,  1, 12, 12,  7,  2 },	// 10
		{ 11, 12, 12, 12, 12, 12, 12 },	// 11
		{  0,  0,  0,  0,  0,  0,  0 },	// 12
		{ 12,  8, 12, 12, 12, 12, 12 }	// 13
	};
	long depth = 0;
	unsigned long node_index = 0;

	char error[BUF_SIZE];

	slist<string> word_list;
	pair<unsigned long, ConfigNode> sibling_node;
	stack<pair<unsigned long, ConfigNode> > sibling_nodes;

	ConfigNode *parent_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
	parent_node->clear();

	ConfigNode *child_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
	child_node->clear();

	for (unsigned long current_state = 0; !tokens.empty(); tokens.pop()) {
		switch (current_state) {
			case 1:
				sibling_node.first = depth;
				sibling_node.second = *child_node;

				sibling_nodes.push(sibling_node);

				child_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
				child_node->clear();
				break;

			case 2:
				sibling_node.first = depth;
				sibling_node.second = *child_node;

				sibling_nodes.push(sibling_node);

				child_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
				child_node->clear();

				depth++;
				break;

			case 4:
				parent_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
				parent_node->clear();

				parent_node->addNode(words.top(), child_node);
				child_node = parent_node;

				words.pop();
				break;

			case 5:
				word_list.push_front(words.top());
				words.pop();
				break;

			case 6:
				for (slist<string>::iterator word = word_list.begin(); word != word_list.end(); ++word) {
					child_node->addNode(*word, &empty_node);
				}
				word_list.clear();
				break;

			case 7:
				while (!sibling_nodes.empty() && sibling_nodes.top().first == (unsigned long) depth) {
					mergeNodes(*child_node, sibling_nodes.top().second);

					sibling_nodes.pop();
				}
				depth--;
				break;

			case 8:
				parent_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
				parent_node->clear();

				parent_node->addNode(words.top(), child_node);
				child_node = parent_node;

				words.pop();
				break;

			case 9:
				sibling_node.first = depth;
				sibling_node.second = *child_node;

				sibling_nodes.push(sibling_node);

				child_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
				child_node->clear();
				break;

			case 10:
				sibling_node.first = depth;
				sibling_node.second = *child_node;

				sibling_nodes.push(sibling_node);

				child_node = &(node[node_index++ % NODE_ARRAY_SIZE]);
				child_node->clear();

				depth++;
				break;

			case 13:
				while (!sibling_nodes.empty() && sibling_nodes.top().first == (unsigned long) depth) {
					mergeNodes(*child_node, sibling_nodes.top().second);

					sibling_nodes.pop();
				}
				depth--;
				break;
		}
		if (next_state[current_state][tokens.top().type] == 12) {
			result = FAILURE;

			switch (current_state) {
				case 0:
					snprintf(error, BUF_SIZE, "line %lu of %s is missing a valid token of ';' or '}'.", tokens.top().line, tokens.top().file.c_str());
					break;

				case 1:
					snprintf(error, BUF_SIZE, "line %lu of %s contains an invalid token within a grouping block (ie: '[ ... ]').", tokens.top().line, tokens.top().file.c_str());
					break;

				case 2:
					snprintf(error, BUF_SIZE, "line %lu of %s is missing a valid token of ';' or '{'.", tokens.top().line, tokens.top().file.c_str());
					break;

				case 3:
					snprintf(error, BUF_SIZE, "line %lu of %s contains an invalid token within a grouping block (ie: '[ ... ]').", tokens.top().line, tokens.top().file.c_str());
					break;

				case 4:
					snprintf(error, BUF_SIZE, "line %lu of %s contains a word token after a grouping block (ie: '[ ... ]').", tokens.top().line, tokens.top().file.c_str());
					break;
				case 5:
					snprintf(error, BUF_SIZE, "line %lu of %s contains an invalid token within a grouping block (ie: '[ ... ]').", tokens.top().line, tokens.top().file.c_str());
					break;

				case 6:
					snprintf(error, BUF_SIZE, "line %lu of %s is missing a valid word token before a grouping block (ie: '[ ... ]').", tokens.top().line, tokens.top().file.c_str());
					break;

				case 7:
					snprintf(error, BUF_SIZE, "line %lu of %s is missing a valid word token before a hierarchy block (ie: '{ ... }').", tokens.top().line, tokens.top().file.c_str());
					break;

				case 8:
					snprintf(error, BUF_SIZE, "line %lu of %s contains a word token after a grouping block (ie: '[ ... ]').", tokens.top().line, tokens.top().file.c_str());
					break;

				case 9:
					snprintf(error, BUF_SIZE, "line %lu of %s contains invalid token '{' within a grouping block (ie: '[ ... ]').", tokens.top().line, tokens.top().file.c_str());
					break;

				case 10:
					snprintf(error, BUF_SIZE, "line %lu of %s is missing a valid token of ';', '{',  or '}'.", tokens.top().line, tokens.top().file.c_str());
					break;

				case 11:
					snprintf(error, BUF_SIZE, "line %lu of %s caused a state machine error (E_STATE_MACHINE_11).", tokens.top().line, tokens.top().file.c_str());
					break;

				case 12:
					snprintf(error, BUF_SIZE, "line %lu of %s caused a state machine error (E_STATE_MACHINE_12).", tokens.top().line, tokens.top().file.c_str());
					break;

				case 13:
					snprintf(error, BUF_SIZE, "line %lu of %s is missing a valid word token before a hierarchy block (: ie'{ ... }').", tokens.top().line, tokens.top().file.c_str());
					break;

				default:
					snprintf(error, BUF_SIZE, "line %lu of %s contains an unknown error.", tokens.top().line, tokens.top().file.c_str());
			}
			errors.push_front(error);
		}
		current_state = next_state[current_state][tokens.top().type];
	}
	mergeNodes(root, *parent_node);

	while (!sibling_nodes.empty()) {
		mergeNodes(root, sibling_nodes.top().second);

		sibling_nodes.pop();
	}
	if (depth < 0) {
		snprintf(error, BUF_SIZE, "configuration file is missing %lu hierarchy block closing token(s) (ie: '}').", 0 - depth);
		errors.push_front(error);

		result = FAILURE;
	} else if (depth > 0) {
		snprintf(error, BUF_SIZE, "configuration file is missing %lu hierarchy block opening token(s) (ie: '{').", depth);
		errors.push_front(error);

		result = FAILURE;
	}
	return result;
}

RESULT substrReplace(string &c, const char *from, const char *to) {
	for (string::iterator it = c.begin(); it != c.end(); ++it) {
		if (*it == ' ') {
			it = c.insert(it, '\\');
			++it;
		}
	}

	return SUCCESS;
}

RESULT ConfigurationParser::mergeNodes(ConfigNode &x, ConfigNode &y) {
	RESULT result = SUCCESS;

	ConfigNode *x2, *y2;
	vector<string> *p;

	if ((p = y.getValues("")) != NULL) {
		for (vector<string>::iterator it = p->begin(); it != p->end(); ++it) {
			substrReplace(*it, " ", "\\ ");

			if ((y2 = y.getNode(*it)) != NULL) {
				if ((x2 = x.getNode(*it)) != NULL) {
					if (result != FAILURE) {

						result = mergeNodes(*x2, *y2);
						x.addNode(*it, x2);
					}
					delete x2;
				} else {
					x.addNode(*it, y2);
				}
				delete y2;
			} else {
				result = FAILURE;
			}
		}	
		delete p;
	}
	return result;
}

RESULT ConfigurationParser::handleToken(stack<config_token> &tokens, stack<string> &words, config_token &token, string &word, char token_char) {
	RESULT result = SUCCESS;

	if (word.size() > 0) {
		words.push(word);
		word.erase();

		token.type = TOKEN_WORD;
		tokens.push(token);
	}
	if (token_char != 0) {
		switch (token_char) {
			case ';':
				token.type = TOKEN_SEMICOLON;
				break;

			case '{':
				token.type = TOKEN_OPEN_BRACKET;
				break;

			case '}':
				token.type = TOKEN_CLOSE_BRACKET;
				break;
				
			case '[':
				token.type = TOKEN_OPEN_BRACE;
				break;

			case ']':
				token.type = TOKEN_CLOSE_BRACE;
				break;

			case -128:
				token.type = TOKEN_EOF;
				break;

			default:
				printf("ERROR: unknown token character '%c'.\n", token_char);
		}
		tokens.push(token);
	}

	return result;
}

RESULT ConfigurationParser::tokenizeConfiguration(slist<string> &errors, stack<config_token> &tokens, stack<string> &words, string configuration_file, long depth) {
	RESULT result = SUCCESS;

	string word, file;

	config_token	token;
	struct stat		file_stat;
	DIR				*directory;
	struct dirent	*directory_entry;

	bool		is_quoted = false;
	bool		is_escaped = false;
	bool		is_line_comment = false;
	bool		is_pound = false;
	bool		is_pound_include = false;

	unsigned long block_comment_depth = 0;

	size_t	read_bytes = 0;
	char		prev_char = 0;

	char		buffer[BUF_SIZE], error[BUF_SIZE];

	token.file = configuration_file;
	token.type = 0;
	token.line = 1;

	if (tokens.empty()) {
		handleToken(tokens, words, token, word, -128);
		handleToken(tokens, words, token, word, -128);
	}
	/* Determine if this is a file or a directory.  If it is a directory, open all files in the
		directory, recursively. */
	if (stat(configuration_file.c_str(), &file_stat) != 0) {
		snprintf(error, BUF_SIZE, "failed to stat file '%s'.", configuration_file.c_str());
		errors.push_front(error);

		return FAILURE;		
	} else {
		if (S_ISDIR(file_stat.st_mode)) {
			/* This is a directory, not a file.  Get all file names in this directory and call
				this function recursively on them. */
			directory = opendir(configuration_file.c_str());		
			if (directory == NULL) {
				snprintf(error, BUF_SIZE, "failed to open directory '%s'.", configuration_file.c_str());
				errors.push_front(error);

				return FAILURE;		
			}
			while (((directory_entry = readdir(directory)) != NULL) && (result != FAILURE)) {
				/* Ignore files that begin with '.' or '~'. */
				if ((directory_entry->d_name[0] != '.') && (directory_entry->d_name[0] != '~')) {
					file = configuration_file;
					file.append("/");
					file.append(directory_entry->d_name);

					TRACE(TRACE_INTERNAL, "Including: %s", file.c_str());
					if ((result = tokenizeConfiguration(errors, tokens, words, file, depth + 1)) == FAILURE) {
						snprintf(error, BUF_SIZE, "failed to include file '%s' at line %lu of file '%s'.", file.c_str(), token.line, configuration_file.c_str());
						errors.push_front(error);
					}
				}
			}
			closedir(directory);

			return result;
		}
	}

	FILE *fp = fopen(configuration_file.c_str(), "r");
	if (fp == NULL) {
		snprintf(error, BUF_SIZE, "failed to open file '%s'.", configuration_file.c_str());
		errors.push_front(error);

		return FAILURE;
	}
	while ((read_bytes = fread(buffer, sizeof(char), BUF_SIZE, fp)) != 0) {
		for (size_t i = 0; i < read_bytes; i++) {
			if (!is_line_comment && (block_comment_depth == 0)) {
				switch (buffer[i]) {
					case '/':
						if (!is_quoted && (prev_char == '/')) {
							is_line_comment = true;
							word.erase(word.size() - 1);
						} else {
							word.push_back(buffer[i]);
						}
						break;

					case '*':
						if (!is_quoted && (prev_char == '/')) {
							word.erase(word.size() - 1);

							block_comment_depth++;
						} else {
							word.push_back(buffer[i]);
						}
						break;

					case '-':
						if (!is_quoted && (prev_char == '-')) {
							is_line_comment = true;
							word.erase(word.size() - 1);
						} else {
							word.push_back(buffer[i]);
						}
						break;

					case '#':
						if (!is_quoted) {
							is_pound = true;
						}

						word.push_back(buffer[i]);
						break;
					
					case '\\':
						if (is_escaped) {
							word.push_back(buffer[i]);
							is_escaped = false;
						} else if (is_quoted) {
							is_escaped = true;
						}
						break;

					case '"':
						if (is_escaped) {
							word.push_back(buffer[i]);
						} else {
							is_quoted = !is_quoted;
						}
						break;
					
					case '{':
					case '}':
					case '[':
					case ']':
					case ';':
						if (!is_quoted) {
							if (handleToken(tokens, words, token, word, buffer[i]) != SUCCESS) {
								result = FAILURE;
							}
						} else {
							word.push_back(buffer[i]);
						}
						break;

					case '\n':
						token.line++;

					case '\t':
					case ' ':
					case '\r':
					case '\0':
						if (is_pound) {
							is_pound = false;
							/* If #include, toggle pound_include, otherwise treat line as comment. */
							if (word.compare("#include") == 0) {
								is_pound_include = true;
							} else {
								is_line_comment = true;
							}
							/* Skip word. */
							word.erase();
							break;
						}

						/* If in pound_include mode, read our file. */
						if (is_pound_include) {
							if (word.size() > 2 && word[0] == '<') { /* Search file path. */
								word.erase(0, 1);
								if (word[word.size() - 1] == '>') {
									word.erase(word.size() - 1, 1);
								}

								file = DEFAULT_CONFIG_DIR;
								file.append(word);
							} else { /* Relative or absolute file path. */
								file = word;
							}

							if (word.size() > 0) {
								/* We only want to pay attention to the first word following the #include. */
								is_pound_include = false;

								/* Are we within our max depth? */
								if (depth >= INCLUDE_MAXDEPTH) {
									result = FAILURE;
									snprintf(error, BUF_SIZE, "maximum include depth of %d exceeded. Refusing to include '%s' at line %lu of file '%s'.", INCLUDE_MAXDEPTH, file.c_str(), token.line, configuration_file.c_str());
									errors.push_front(error);
								} else {
									if ((result = tokenizeConfiguration(errors, tokens, words, file, depth + 1)) == FAILURE) {
										snprintf(error, BUF_SIZE, "failed to include file '%s' at line %lu of file '%s'.", file.c_str(), token.line, configuration_file.c_str());
										errors.push_front(error);
									}
								}

								/* Skip the rest of the line, if it isn't the end of the line. */
								if (buffer[i] != '\n') {
									is_line_comment = true;
								}
							} else if (buffer[i] == '\n') {
								/* EOL.. disable pound_include */
								is_pound_include = false;
							}

							/* Skip word. */
							word.erase();
							break;
						}

						if (!is_quoted) {
							if (handleToken(tokens, words, token, word, 0) != SUCCESS) {
								result = FAILURE;
							}
						} else {
							word.push_back(buffer[i]);
						}
						break;

					case 't':
						if (is_quoted && is_escaped) {
							word.push_back(9);
							is_escaped = false;
						} else {
							word.push_back(buffer[i]);
						}
						break;

					case 'r':
						if (is_quoted && is_escaped) {
							word.push_back(13);
							is_escaped = false;
						} else {
							word.push_back(buffer[i]);
						}
						break;

					case 'n':
						if (is_quoted && is_escaped) {
							word.push_back(10);
							is_escaped = false;
						} else {
							word.push_back(buffer[i]);
						}
						break;

					default:
						word.push_back(buffer[i]);
				}
			} else {
				switch (buffer[i]) {
					case '\n':
						is_pound = false;
						is_pound_include = false;
						is_line_comment = false;
						token.line++;
						break;

					case '/':
						if (prev_char == '*') {
							block_comment_depth--;
						}
						break;
				
					case '*':
						if (prev_char == '/') {
							block_comment_depth++;
						}	
				}
			}
			if (is_escaped && buffer[i] != '\\') {
				is_escaped = false;
			}
			prev_char = buffer[i];
		}
	}
	fclose(fp);

	return result;
}

/*
** Local Variables:
** c-basic-offset: 3
** tab-width: 3
** End:
** vim:
** vim: noet ts=3 sw=3
*/
