///                _       _
/// __  _____ _ __(_)_ __ | |_
/// \ \/ / __| '__| | '_ \| __|
///  >  < (__| |  | | |_) | |_
/// /_/\_\___|_|  |_| .__/ \__|
///                 |_|
///
/// Copyright (c) 1997 - Ognjen 'xolatile' Milan Robovic
///
/// xolatile@chud.cyou - xcript - Whitespace insignificant INI/CFG-like script parser.
///
/// This program is free software, free as in freedom and as in free beer, 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 3 of the License, or any later version if you wish...
///
/// This program is distributed in the hope that it will be useful, but it is probably not, and without any warranty, without even the implied
/// warranty of merchantability or fitness for a particular purpose, because it is pointless. Please see the GNU (Geenoo) General Public License
/// for more details, if you dare, it is a lot of text that nobody wants to read...

typedef enum {
	script_unknown, script_comment, script_string,  script_number,  script_marker,  script_header,  script_assign,  script_end,
	script_from,    script_to,      script_next
} script_word_type;

typedef struct {
	character * path;
	character * source;
	natural     prefix;
	natural     length;
	natural     suffix;
	natural     offset;
	natural     line;
	natural     last_length;
	character * last_string;
	boolean     force;
	boolean     range;
} script_data_structure;

typedef struct {
	natural       counter;
	character * * identifier;
	natural     * index;
} script_structure;

static procedure script_warning (script_data_structure * script, boolean condition, character * message) {
	if (condition == true) {
		print ("/w %s: %i: %s\n", script->path, script->line, message);
	}
}

static procedure script_failure (script_data_structure * script, boolean condition, character * message) {
	if (condition == true) {
		print ("/f %s: %i: %s\n", script->path, script->line, message);

		print ("/1%s/-", & script->source [script->offset]);

		exit (log_failure);
	}
}

static script_data_structure * script_open (character * path) {
	script_data_structure * script = allocate (sizeof (* script));

	script->path   = string_duplicate (path);
	script->source = file_import      (path);

	script->prefix      = 0;
	script->length      = 0;
	script->suffix      = 0;
	script->offset      = 0;
	script->line        = 1;
	script->last_length = 0;
	script->last_string = & script->source [0];

	return (script);
}

static script_data_structure * script_close (script_data_structure * script) {
	script->path   = deallocate (script->path);
	script->source = deallocate (script->source);

	return (deallocate (script));
}

static boolean script_compare (script_data_structure * script, character * string) {
	return (string_compare_limit (string, script->last_string, script->last_length));
}

static boolean script_check (script_structure * information, natural index, character * identifier) {
	return (string_compare (identifier, information->identifier [index]));
}

static character * script_export_string (script_data_structure * script) {
	return (string_duplicate_limit (script->last_string, script->last_length));
}

static natural script_export_number (script_data_structure * script) {
	return (string_limit_to_number (script->last_string, script->last_length));
}

static natural script_export_marker (script_structure * information, script_data_structure * script) {
	for (natural counter = 0; counter < information->counter; ++counter) {
		if (script_compare (script, information->identifier [counter]) == true) {
			return (information->index [counter]);
		}
	}

	script_failure (script, true, "No such identifier defined so far in any of the headers!");

	return (~ 0u);
}

static script_word_type script_parser (script_data_structure * script) {
	script_word_type word = script_unknown;

	script->prefix = 0;
	script->length = 0;
	script->suffix = 0;

	for (; character_is_blank (script->source [script->offset + script->prefix]) == true; ++script->prefix) {
		if (script->source [script->offset + script->prefix] == '\n') {
			++script->line;
		}
	}

	if (script->source [script->offset + script->prefix] == '\0') {
		word = script_end;
	} else if (script->source [script->offset + script->prefix] == '(') {
		script_failure (script, script->range == true, "You are already defining a range, only one pair of () is allowed.");
		script->range = true;
		++script->length;
		word = script_from;
	} else if (script->source [script->offset + script->prefix] == ',') {
		script_failure (script, script->range == false, "You can't use ',' outside of a range.");
		++script->length;
		word = script_next;
	} else if (script->source [script->offset + script->prefix] == ')') {
		script_failure (script, script->range == false, "You already defined a range, only one pair of () is allowed.");
		script->range = false;
		++script->length;
		word = script_to;
	} else if (script->source [script->offset + script->prefix] == ';') {
		for (; script->source [script->offset + script->prefix + script->length] != '\n'; ++script->length) {
			script_warning (script, script->source [script->offset + script->prefix + script->length] == '\0',
			                "Expected at least a trailing new line or some blank character after a comment!");
		}
		word = script_comment;
	} else if (script->source [script->offset + script->prefix] == '#') {
		for (; script->source [script->offset + script->prefix + script->length] != '\n'; ++script->length) {
			script_warning (script, script->source [script->offset + script->prefix + script->length] == '\0',
			                "Expected at least a trailing new line or some blank character after a comment!");
		}
		word = script_comment;
	} else if (script->source [script->offset + script->prefix] == '=') {
		++script->length;
		word = script_assign;
	} else if (script->source [script->offset + script->prefix] == '"') {
		script_failure (script, script->range == true, "You can't use string inside of a range.");
		for (script->length = 1; script->source [script->offset + script->prefix + script->length] != '"'; ++script->length) {
			script_failure (script, script->source [script->offset + script->prefix + script->length] == '\0',
			                "Unterminated string literal, missing '\"' character.");
		}
		++script->prefix;
		--script->length;
		++script->suffix;
		word = script_string;
	} else if (script->source [script->offset + script->prefix] == '\'') {
		script_failure (script, script->range == true, "You can't use string inside of a range.");
		for (script->length = 1; script->source [script->offset + script->prefix + script->length] != '\''; ++script->length) {
			script_failure (script, script->source [script->offset + script->prefix + script->length] == '\0',
			                "Unterminated string literal, missing ''' character.");
		}
		++script->prefix;
		--script->length;
		++script->suffix;
		word = script_string;
	} else if (script->source [script->offset + script->prefix] == '[') {
		script_failure (script, script->range == true, "You can't use header inside of a range.");
		for (; script->source [script->offset + script->prefix + script->length] != ']'; ++script->length) {
			script_failure (script, script->source [script->offset + script->prefix + script->length] == '\0',
			                "Unterminated header element, missing ']' character.");
		}
		++script->prefix;
		--script->length;
		++script->suffix;
		word = script_header;
	} else if (character_is_digit (script->source [script->offset + script->prefix]) == true) {
		for (; character_is_digit (script->source [script->offset + script->prefix + script->length]) == true; ++script->length) {
			script_warning (script, script->source [script->offset + script->prefix + script->length] == '\0',
			                "Expected at least a trailing new line or some blank character after a number!");
		}
		word = script_number;
	} else if (character_is_identifier (script->source [script->offset + script->prefix]) == true) {
		for (; character_is_identifier (script->source [script->offset + script->prefix + script->length]) == true; ++script->length) {
			script_warning (script, script->source [script->offset + script->prefix + script->length] == '\0',
			                "Expected at least a trailing new line or some blank character after a marker!");
		}
		word = script_marker;
	} else {
		script_failure (script, true, format ("Illegal character '%c' in script.", script->source [script->offset + script->prefix]));
	}

	script->last_string = & script->source [script->offset + script->prefix];
	script->last_length =   script->length;

	script->offset += script->prefix + script->length + script->suffix;

	return (word);
}

static character * script_expect_header (script_structure * information, script_data_structure * script, natural index, boolean accept) {
	if (accept == true) {
		++information->counter;

		information->identifier = reallocate (information->identifier, information->counter * sizeof (* information->identifier));
		information->index      = reallocate (information->index,      information->counter * sizeof (* information->index));

		information->identifier [information->counter - 1] = string_duplicate_limit (script->last_string, script->last_length);
		information->index      [information->counter - 1] = index;
	}

	return (script_export_string (script));
}

static character * script_expect_string (script_data_structure * script) {
	script_word_type word = script_unknown;

	script_failure (script, (word = script_parser (script)) != script_assign, "Expected '=', assignment operator.");
	script_failure (script, (word = script_parser (script)) != script_string, "Expected string literal.");

	return (script_export_string (script));
}

static natural script_expect_number (script_data_structure * script) {
	script_word_type word = script_unknown;

	script_failure (script, (word = script_parser (script)) != script_assign, "Expected '=', assignment operator.");
	script_failure (script, (word = script_parser (script)) != script_number, "Expected number literal.");

	return (script_export_number (script));
}

static natural script_expect_marker (script_structure * information, script_data_structure * script) {
	script_word_type word = script_unknown;

	script_failure (script, (word = script_parser (script)) != script_assign, "Expected '=', assignment operator.");
	script_failure (script, (word = script_parser (script)) != script_marker, "Expected marker literal.");

	return (script_export_marker (information, script));
}

static natural script_expect_number_or_marker (script_structure * information, script_data_structure * script) {
	script_word_type word = script_unknown;

	script_failure (script, (word = script_parser (script)) != script_assign, "Expected '=', assignment operator.");

	word = script_parser (script);

	if (word == script_number) {
		return (script_export_number (script));
	} else if (word == script_marker) {
		return (script_export_marker (information, script));
	} else {
		script_failure (script, true, "Expected number or marker literal.");
	}

	return (~ 0u);
}

static natural * script_expect_ordered_array (script_structure * information, script_data_structure * script, natural * count) {
	script_word_type word = script_unknown;

	natural   found = 0;
	natural * array = null;

	script_failure (script, (word = script_parser (script)) != script_assign, "Expected '=', assignment operator.");
	script_failure (script, (word = script_parser (script)) != script_from,   "Expected '(', begin range operator.");

	for (word = script_parser (script); word != script_to; word = script_parser (script)) {
		++found;

		array = reallocate (array, found * sizeof (* array));

		if (word == script_number) {
			array [found - 1] = script_export_number (script);
		} else if (word == script_marker) {
			array [found - 1] = script_export_marker (information, script);
		} else {
			script_failure (script, true, "Expected number or marker!");
		}

		if ((word = script_parser (script)) == script_to) break;

		script_failure (script, word != script_next, "Expected ranged next ','.");
		script_failure (script, word == script_end,  "Expected ranged to ')'.");
	}

	(* count) = found;

	return (array);
}

static natural * script_expect_unordered_array (script_structure * information, script_data_structure * script, natural count) {
	script_word_type word = script_unknown;

	natural * array = allocate (count * sizeof (* array));

	script_failure (script, (word = script_parser (script)) != script_assign, "Expected '=', assignment operator.");
	script_failure (script, (word = script_parser (script)) != script_from,   "Expected '(', begin range operator.");

	for (word = script_parser (script); word != script_to; word = script_parser (script)) {
		natural index = script_export_marker (information, script);

		script_failure (script, word != script_marker, "Expected ranged marker.");

		script_failure (script, (word = script_parser (script)) != script_assign, "Expected '=', assignment operator.");

		word = script_parser (script);

		if (word == script_number) {
			array [index] = script_export_number (script);
		} else if (word == script_marker) {
			array [index] = script_export_marker (information, script);
		} else {
			script_failure (script, true, "Expected number or marker!");
		}

		if ((word = script_parser (script)) == script_to) break;

		script_failure (script, word != script_next, "Expected ranged next ','.");
		script_failure (script, word == script_end,  "Expected ranged to ')'.");
	}

	return (array);
}

static script_structure * script_initialize (character * general_script_file_path) {
	script_structure * script = allocate (sizeof (* script));

	script_word_type word = script_unknown;

	script_data_structure * general = script_open (general_script_file_path);

	for (word = script_parser (general); word != script_end; word = script_parser (general)) {
		if (word == script_header) {
			++script->counter;
			script->identifier = reallocate (script->identifier, script->counter * sizeof (* script->identifier));
			script->index      = reallocate (script->index,      script->counter * sizeof (* script->index));
			script->identifier [script->counter - 1] = string_duplicate_limit (general->last_string, general->last_length);
			script->index      [script->counter - 1] = script->counter - 1;
		} else if ((word == script_end) || (word == script_comment)) {
			continue;
		} else {
			script_failure (general, true, "Expected header in general script.");
		}
	}

	general = script_close (general);

	return (script);
}

static script_structure * script_deinitialize (script_structure * script) {
	for (natural index = 0; index < script->counter; ++index) {
		script->identifier [index] = deallocate (script->identifier [index]);
	}

	script->identifier = deallocate (script->identifier);
	script->index      = deallocate (script->index);

	return (deallocate (script));
}

static natural script_indexer (script_structure * information, character * identifier) {
	for (natural counter = 0; counter < information->counter; ++counter) {
		if (string_compare (identifier, information->identifier [counter]) == true) {
			return (information->index [counter]);
		}
	}

	fatal_failure (true, "script_indexer: No such identifier defined so far in any of the headers!");

	return (~ 0u);
}