<?php

/****************************************************************************
 * XML CONFIGURATION PARSER
 ****************************************************************************/

class xml_config {

	private static $mVersion	= null;
	private static $mGame		= null;
	private static $versions	= null;
	private static $games		= null;
	private static $defGame		= null;

    private static function readFromEnv(string $varName, string $default): string {
        if ($varName == '') {
            return $default;
        }

        $fileVarName = $varName . '_FILE';
        $fileName = getenv($fileVarName);
        if ($fileName !== false) {
            $value = @file_get_contents($fileName);
        } else {
            $value = getenv($varName);
        }
        return $value ?: $default;
    }

	private static function parseMainParams($root) {
		$node = $root->firstChild;

		while ($node) {
			if ($node->nodeType == XML_ELEMENT_NODE && $node->nodeName == 'Param') {
				$aName = $node->getAttribute('name');
				if ($aName == '') {
					l::warn("CONFIG: a main parameter is missing a 'name' attribute");
				} elseif (!isset(xml_config::$mGame->params[$aName])) {
                    $aValue = $node->getAttribute('value');
                    $aFromEnv = $node->getAttribute('from-env');
                    $aValue = self::readFromEnv($aFromEnv, $aValue);
					xml_config::$mGame->params[$aName] = $aValue;
				} else {
					l::notice("CONFIG: duplicate main parameter '$aName'");
				}
			} elseif ($node->nodeType == XML_ELEMENT_NODE) {
				l::notice("CONFIG: found unexpected tag '{$node->nodeName}' in MainParams section");
			}
			$node = $node->nextSibling;
		}
	}


	private static function parseMainTicks($root) {
		$node = $root->firstChild;

		while ($node) {
			if ($node->nodeType == XML_ELEMENT_NODE && $node->nodeName == 'Tick') {
				$tScript	= $node->getAttribute('script');
				$tFirst		= $node->getAttribute('first');
				$tInterval	= $node->getAttribute('interval');
				$tLast		= $node->getAttribute('last');
				if ($tScript == "" || $tFirst == "" || $tInterval == "") {
					l::warn("CONFIG: a main tick declaration is missing a mandatory attribute");
				} elseif ((int)$tInterval == 0) {
					l::warn("CONFIG: invalid interval for main tick '$tScript'");
				} elseif (is_null(xml_config::$mVersion->getTickDefinition($tScript))) {
					new tick_definition(xml_config::$mVersion, $tScript, false);
					new tick_instance(xml_config::$mGame, $tScript, (int)$tFirst,
						(int)$tInterval, $tLast ? (int)$tLast : null);
				} else {
					l::notice("CONFIG: duplicate main tick '$tScript'");
				}
			} elseif ($node->nodeType == XML_ELEMENT_NODE) {
				l::notice("CONFIG: found unexpected tag '{$node->nodeName}' in MainTicks section");
			}
			$node = $node->nextSibling;
		}
	}


	private static function parseTickDefinition($version, $root) {
		$script = $root->getAttribute('script');
		$public = ($root->getAttribute('public') == 1);

		if ($script == "") {
			l::warn("CONFIG: invalid tick definition for version {$version->id}");
			return;
		}
		if (!is_null($version->getTickDefinition($script))) {
			l::notice("CONFIG: duplicate tick definition '$script' for version {$version->id}");
			return;
		}

		$def = new tick_definition($version, $script, $public);
		if (!$public) {
			return;
		}

		$names = array();
		$descs = array();
		$node = $root->firstChild;
		while ($node) {
			if ($node->nodeType != XML_ELEMENT_NODE) {
				$node = $node->nextSibling;
				continue;
			}

			if ($node->nodeName != "Name" && $node->nodeName != 'Description') {
				l::warn("CONFIG: unexpected tag '{$node->nodeName}' found in tick definition "
					. "'{$script}' for version '{$version->id}'");
				$node = $node->nextSibling;
				continue;
			}

			$lang = $node->getAttribute('lang');
			if ($lang == '') {
				l::warn("CONFIG: missing language for {$node->nodeName} in tick definition "
					. "'{$script}' for version '{$version->id}'");
				$node = $node->nextSibling;
				continue;
			}

			$contents = trim($node->textContent);
			if ($contents == '') {
				l::warn("CONFIG: missing contents for {$node->nodeName} in tick definition "
					. "'{$script}' for version '{$version->id}'");
				$node = $node->nextSibling;
				continue;
			}

			if ($node->nodeName == 'Name') {
				$names[$lang] = $contents;
			} else {
				$descs[$lang] = $contents;
			}

			$node = $node->nextSibling;
		}

		foreach ($names as $lang => $name) {
			$def->addText($lang, $name, $descs[$lang]);
		}
	}


	private static function parseVersion($root) {
		$id = $root->getAttribute("id");
		$cp = ($root->getAttribute("playable") == 1);
		$tx = $root->getAttribute("text");

		if ($id == "" || $tx == "") {
			l::warn("CONFIG: invalid version definition (missing identifier or text)");
			return null;
		} elseif ($id == 'main') {
			l::warn("CONFIG: invalid version definition (using 'main' identifier)");
			return null;
		}

		$version = new version($id, $cp, $tx);

		$node = $root->firstChild;
		while ($node) {
			if ($node->nodeType == XML_ELEMENT_NODE && $node->nodeName == 'Tick') {
				xml_config::parseTickDefinition($version, $node);
			} elseif ($node->nodeType == XML_ELEMENT_NODE) {
				l::warn("CONFIG: found unexpected tag '{$node->nodeName}' in MainTicks section");
			}
			$node = $node->nextSibling;
		}

		return $version;
	}


	private static function parseVersions($root) {
		$node = $root->firstChild;

		$versions = array();
		while ($node) {
			if ($node->nodeType == XML_ELEMENT_NODE && $node->nodeName == 'Version') {
				$v = xml_config::parseVersion($node);
				if (!is_null($v)) {
					if (isset($versions[$v->id])) {
						l::notice("CONFIG: found duplicate definition for version '{$v->id}'");
					} else {
						$versions[$v->id] = $v;
					}
				}
			} elseif ($node->nodeType == XML_ELEMENT_NODE) {
				l::notice("CONFIG: found unexpected tag '{$node->nodeName}' in Versions section");
			}
			$node = $node->nextSibling;
		}

		return $versions;
	}


	private static function parseGame($root) {
		$id = $root->getAttribute('id');
		if ($id == '') {
			l::warn("CONFIG: game definition is missing an identifier");
			return null;
		}
		if ($id == 'main') {
			l::warn("CONFIG: game definition with 'main' identifier ignored");
			return null;
		}

		$version = $root->getAttribute('version');
		if (is_null(xml_config::$versions[$version])) {
			l::warn("CONFIG: game '$id' has unknown version '$version'");
			return null;
		}

		$namespace = $root->getAttribute('namespace');
		if ($namespace == '') {
			l::warn("CONFIG: no namespace defined for game '$id'");
			return null;
		}
		$text = $root->getAttribute('text');
		if ($namespace == '') {
			l::warn("CONFIG: no text for game '$id'");
			return null;
		}

		$public = ($root->getAttribute('public') == 1);
		$canJoin = ($root->getAttribute('canjoin') == 1);

		$game = new game(xml_config::$versions[$version], $id, $namespace, $public, $canJoin, $text);

		$node = $root->firstChild;
		while ($node) {
			if ($node->nodeType != XML_ELEMENT_NODE) {
				$node = $node->nextSibling;
				continue;
			}

			if ($node->nodeName == 'Param') {
				$aName = $node->getAttribute('name');
				$aValue = $node->getAttribute('value');
				if ($aName == "") {
					l::warn("CONFIG: a parameter is missing a 'name' attribute for game '$id'");
				} elseif (isset($game->params[$aName])) {
					l::notice("CONFIG: duplicate parameter '$aName'");
				} else {
					$game->params[$aName] = $aValue;
				}
			} elseif ($node->nodeName == 'Description') {
				$lang = $node->getAttribute('lang');
				$contents = trim($node->textContent);
				if ($lang == "") {
					l::warn("CONFIG: a description is missing the 'lang' attribute for game '$id'");
				} elseif ($contents == "") {
					l::warn("CONFIG: description in language '$lang' has no contents for game '$id'");
				} elseif (isset($game->descriptions[$lang])) {
					l::notice("CONFIG: description in language '$lang' appears twice for game '$id'");
				} else {
					$game->descriptions[$lang] = $contents;
				}
			} elseif ($node->nodeName == 'Tick') {
				$tScript	= $node->getAttribute('script');
				$tFirst		= $node->getAttribute('first');
				$tInterval	= $node->getAttribute('interval');
				$tLast		= $node->getAttribute('last');
				if ($tScript == "" || $tFirst == "" || $tInterval == "") {
					l::warn("CONFIG: a tick declaration is missing a mandatory attribute "
						. "for game '$id'");
				} elseif ((int)$tInterval == 0) {
					l::warn("CONFIG: invalid interval for tick '$tScript' in game '$id'");
				} elseif (is_null($game->version->getTickDefinition($tScript))) {
					l::warn("CONFIG: no definition for tick '$tScript' in game '$id' "
						. "(version '$version')");
				} elseif (isset($game->ticks[$tScript])) {
					l::notice("CONFIG: duplicate tick initialiser for tick '$tScript' in game '$id'");
				} else {
					new tick_instance($game, $tScript, (int)$tFirst, (int)$tInterval,
						$tLast ? (int)$tLast : null);
				}
			} else {
				l::notice("CONFIG: found unexpected tag '{$node->nodeName}' in game '$id' definition");
			}

			$node = $node->nextSibling;
		}

		return $game;
	}


	private static function parseGames($root) {
		$defaultId = $root->getAttribute('default');
		$games = array();

		$node = $root->firstChild;
		while ($node) {
			if ($node->nodeType == XML_ELEMENT_NODE && $node->nodeName == 'Game') {
				$g = xml_config::parseGame($node);
				if (!is_null($g)) {
					if (isset($games[$g->name])) {
						l::notice("CONFIG: found duplicate definition for game '{$v->name}'");
					} else {
						$games[$g->name] = $g;
					}
				}
			} elseif ($node->nodeType == XML_ELEMENT_NODE) {
				l::notice("CONFIG: found unexpected tag '{$node->nodeName}' in Games section");
			}
			$node = $node->nextSibling;
		}

		if (count($games) && ($defaultId == '' || is_null($games[$defaultId]))) {
			$defaultId = $games[0]->name;
			l::notice("CONFIG: no default game, using '$defaultId'");
		}
		xml_config::$defGame = $defaultId;

		return $games;
	}


	public static function parse($xmlData) {
		$doc = new DOMDocument();
		if (!$doc->loadXML($xmlData)) {
			l::error("CONFIG: error while parsing XML configuration");
			return false;
		}

		xml_config::$mVersion = new version('main', false, 'Legacy Worlds');
		xml_config::$mGame = new game(xml_config::$mVersion, 'main', '', false, false, 'Legacy Worlds');
		xml_config::$versions = null;
		xml_config::$games = null;
		xml_config::$defGame = null;

		$root = $doc->documentElement;
		$node = $root->firstChild;
		while ($node) {
			if ($node->nodeType == XML_ELEMENT_NODE) {
				switch ($node->nodeName) :
				case 'MainParams':
					xml_config::parseMainParams($node);
					break;

				case 'MainTicks':
					xml_config::parseMainTicks($node);
					break;

				case 'Versions':
					if (is_array(xml_config::$versions)) {
						l::notice("CONFIG: found duplicate set of version definitions");
					} else {
						xml_config::$versions = xml_config::parseVersions($node);
					}
					break;

				case 'Games':
					if (is_array(xml_config::$games)) {
						l::notice("CONFIG: found duplicate set of game definitions");
					} elseif (is_array(xml_config::$versions)) {
						xml_config::$games = xml_config::parseGames($node);
					} else {
						l::notice("CONFIG: game definitions found before version "
							. "definitions, ignored");
					}
					break;

				default:
					l::notice("CONFIG: found unknown tag '{$node->nodeName}' at toplevel");
					break;
				endswitch;
			}
			$node = $node->nextSibling;
		}

		if (!is_array(xml_config::$versions) || count(xml_config::$versions) == 0) {
			l::error("CONFIG: no versions have been defined!");
			return false;
		}

		if (!is_array(xml_config::$games) || count(xml_config::$games) == 0) {
			l::error("CONFIG: no games have been defined!");
			return false;
		}

		xml_config::$versions['main'] = xml_config::$mVersion;
		xml_config::$games['main'] = xml_config::$mGame;
		return new config(xml_config::$versions, xml_config::$games, xml_config::$defGame);
	}

}

?>