<?php

define("ONLY_CONTENT", 1);
define("ONLY_COMMENTS", 2);
define("CONTENT_AND_COMMENTS", 3);

function checkXml() {
	global $xml, $db1Name, $db2Name, $tablesInDb1, $tablesInDb2, $typesInDb1, $typesInDb2;
	
	$typesInXml = getTypesInXml($xml);
	writeLog("TYPES IN XML:\n" . arrayToString($typesInXml, "\n", "  ") . "\n");

	$typesInDb1 = getTypesInDb($db1Name);
	writeLog("TYPES IN DB1:\n" . arrayToString($typesInDb1, "\n", "  ") . "\n");
	
	$typesInDb2 = getTypesInDb($db2Name);
	writeLog("TYPES IN DB2:\n" . arrayToString($typesInDb2, "\n", "  ") . "\n");
	
	$tablesInXml = getTablesInXml($xml);
	writeLog("TABLES IN XML:\n" . arrayToString($tablesInXml, "\n", "  ") . "\n");

	$tablesInDb1 = getTablesInDb($db1Name);
	writeLog("TABLES IN DB1:\n" . arrayToString($tablesInDb1, "\n", "  ") . "\n");

	$tablesInDb2 = getTablesInDb($db2Name);
	writeLog("TABLES IN DB2:\n" . arrayToString($tablesInDb2, "\n", "  ") . "\n");

	// check for differences between db1 and db2 content-types (fewer content-types is dangerous, more content-types is not)
	$diff = array_diff($typesInDb2, $typesInDb1);
	if (count($diff) > 0) {
		echo "WARNING! The following content-types in db2 no longer exist in db1:\n";
		echo arrayToString($diff, "\n", "  ");
		echo "Data from those content-types will not be imported. Do you wish to continue (y/n)?";
		if (trim(fgets(STDIN)) != "y")
			trigger_error("User cancelled", E_USER_ERROR);
	}

	// check for differences between db1 and db2 tables (fewer tables is dangerous, more tables is not)
	$diff = array_diff($tablesInDb2, $tablesInDb1);
	if (count($diff) > 0) {
		echo "WARNING! The following tables in db2 no longer exist in db1:\n";
		echo arrayToString($diff, "\n", "  ");
		echo "Data from those tables will not be imported. Do you wish to continue (y/n)?";
		if (trim(fgets(STDIN)) != "y")
			trigger_error("User cancelled", E_USER_ERROR);
	}

	// check for missing content-types in the XML file
	$diff = array_diff($typesInDb1, $typesInXml);
	if (count($diff) > 0)
		trigger_error("Import statement missing in <Imports> section for the following content-types: '" . arrayToString($diff) . "'", E_USER_ERROR);

	// check for missing tables in the XML file
	$diff = array_diff($tablesInDb1, $tablesInXml);
	if (count($diff) > 0)
		trigger_error("Import statement missing in <Imports> section for the following tables: '" . arrayToString($diff) . "'", E_USER_ERROR);
}

function checkNodeConsistency() {
	global $db1Name, $db2Name, $xml;

	writeLog("CHECK NODE CONSISTENCY:\n");

	$results = $xml->xpath("//Imports/Content-type");
	for ($i = 0; $i < count($results); $i++) {
		$typeName = (string)$results[$i]['name'];
		$contentFrom = (string)$results[$i]['contentFrom'];
		$commentsFrom = (string)$results[$i]['commentsFrom'];

		if ($contentFrom == "db1" && $commentsFrom == "db2") {
			// Check if any node was deleted or changed its nid or name
			$rs = execSql(
				"SELECT " . $db2Name . ".node.nid AS nid, " . $db2Name . ".node.title AS title " .
				"FROM " . $db2Name . ".node LEFT JOIN " . $db1Name . ".node " .
				"ON " . $db2Name . ".node.nid = " . $db1Name . ".node.nid " .
				"AND " . $db2Name . ".node.title = " . $db1Name . ".node.title " .
				"WHERE (" . $db1Name . ".node.nid IS NULL " .
				"OR " . $db1Name . ".node.title IS NULL) " .
				"AND " . $db2Name . ".node.type = '" . sqlType($typeName) . "'");
		
			$count = 0;
			while ($row = fetchNextRow($rs)) {
				$nid = $row["nid"];
				$title = $row["title"];

				echo "NID: " . $nid . " TITLE: '" . $title . "' not found in db1\n";
				$count += 1;
			}
			
			freeResult($rs);

			if ($count > 0) {
				echo $count . " nodes of content-type '" . $typeName . "' were deleted or changed their nid or title, some comments could become orphaned. Do you wish to continue (y/n)?";
				if (trim(fgets(STDIN)) != "y")
					trigger_error("User cancelled", E_USER_ERROR);
			}
		}
	}
}

function checkForumConsistency() {
	global $db1Name, $db2Name, $xml;

	writeLog("CHECK FORUM CONSISTENCY:\n");

	// forum sections
	$results = $xml->xpath("//Imports/Table[@name='term_data']");
	$from = (string)$results[0]['from'];

	// forum topics
	$results = $xml->xpath("//Imports/Content-type[@name='forum']");
	$contentFrom = (string)$results[0]['contentFrom'];

	if ($from == "db1" && $contentFrom == "db2" ||
		$from == "db2" && $contentFrom == "db1") {

		// Check if any forum_count was deleted or changed its tid or name
		$rs = execSql(
			"SELECT " . $db2Name . ".term_data.tid AS tid, " . $db2Name . ".term_data.name AS name " .
			"FROM " . $db2Name . ".term_data LEFT JOIN " . $db1Name . ".term_data " .
			"ON " . $db2Name . ".term_data.tid = " . $db1Name . ".term_data.tid " .
			"AND " . $db2Name . ".term_data.name = " . $db1Name . ".term_data.name " .
			"WHERE " . $db1Name . ".term_data.tid IS NULL " .
			"OR " . $db1Name . ".term_data.name IS NULL");
	
		$count = 0;
		while ($row = fetchNextRow($rs)) {
			$tid = $row["tid"];
			$name = $row["name"];

			echo "TID: " . $tid . " NAME: '" . $name . "' not found in db1\n";
			$count += 1;
		}
		
		freeResult($rs);

		if ($count > 0) {
			echo $count . " forum were deleted or changed their tid or name, some topics could become orphaned. Do you wish to continue (y/n)?";
			if (trim(fgets(STDIN)) != "y")
				trigger_error("User cancelled", E_USER_ERROR);
		}
	}
}

function checkNotSupported($dbName) {
	// Check the following tables are always empty (otherwise some special treatment
	// during the delete and copy operations would be probably required)
	writeLog("CHECK TABLES REQUIRED TO BE EMPTY:\n");

	$tables = array(
		'node_counter',
		'poll',
		'poll_choices',
		'poll_votes');

	for ($i = 0; $i < count($tables); $i++) {
		$rs = execSql(
			"SELECT COUNT(*) AS row_count " .
			"FROM " . $dbName . "." . $tables[$i]);
	
		$row = fetchOneRow($rs);
		$row_count = @$row["row_count"];
	
		if ($row_count > 0)
			trigger_error("Not supported: table " . $tables[$i] . " is required to be empty", E_USER_ERROR);
	}

	// Check the 'nid' and 'vid' columns are always identical (otherwise some special
	// treatment during the delete and copy operations would be probably required)
	writeLog("CHECK TABLES REQUIRED TO HAVE IDENTICAL NID AND VID:\n");

	$tables = array(
		'forum',
		'node',
		'node_content_*',
		'node_data_field_*',
		'node_revisions');

	$resultTables = array();
	for ($i = 0; $i < count($tables); $i++) {
		if (substr($tables[$i], strlen($tables[$i]) - 1, 1) == "*") {
			$expandTables = expandWildcard($dbName, $tables[$i]);
			$resultTables = array_merge($resultTables, $expandTables);
		}
		else {
			$resultTables[] = $tables[$i];
		}
	}
	$tables = array_values($resultTables); // the call to array_values forces index recalculation (as it creates a new array)

	for ($i = 0; $i < count($tables); $i++) {
		$rs = execSql(
			"SELECT COUNT(*) AS row_count " .
			"FROM " . $dbName . "." . $tables[$i] . " " .
			"WHERE " . $dbName . "." . $tables[$i] . ".nid <> " . $dbName . "." . $tables[$i] . ".vid ");
	
		$row = fetchOneRow($rs);
		$row_count = @$row["row_count"];
	
		if ($row_count > 0)
			trigger_error("Not supported: table " . $tables[$i] . " is required to have identical nid and vid", E_USER_ERROR);
	}
}

function importTables() {
	global $xml, $db1Name, $db2Name, $tablesInDb1, $tablesInDb2;

	$delTables = array(); // tables which will be deleted from db3
	$copyTables = array(); // tables which will be copied from db2 into db3

	$results = $xml->xpath("//Imports/Table");
	for ($i = 0; $i < count($results); $i++) {
		$tableName = (string)$results[$i]['name'];
		$from = (string)$results[$i]['from'];
		$mandatory = (string)$results[$i]['mandatory'];

		if ($mandatory == "true" || $mandatory == "false")
			$mandatory = ($mandatory == "true");
		else
			trigger_error("Attribute 'mandatory' is missing or invalid value in <Imports> section, table '" . $tableName . "'", E_USER_ERROR);

		if ($from == "db1") {
			if ($mandatory && array_search($tableName, $tablesInDb1) === FALSE)
				trigger_error("Table '" . $tableName . "' specified in the <Imports> section does not exist in db1", E_USER_ERROR);

			// nothing to do (all tables are initially copied from db1 when database is replicated into db3)
		}
		else if ($from == "db2") {
			if ($mandatory && array_search($tableName, $tablesInDb2) === FALSE)
				trigger_error("Table '" . $tableName . "' specified in the <Imports> section does not exist in db2", E_USER_ERROR);
			
			if ($mandatory && array_search($tableName, $tablesInDb1) === FALSE)
				trigger_error("Table '" . $tableName . "' specified in the <Imports> section does not exist in db1; table schema is always imported from db1 even when data is imported from db2.", E_USER_ERROR);
		
			$delTables[] = $tableName;
			$copyTables[] = $tableName;
		}
		else if ($from == "empty") {
			if ($mandatory && array_search($tableName, $tablesInDb1) === FALSE)
				trigger_error("Table '" . $tableName . "' specified in the <Imports> section does not exist in db1; table schema is always imported from db1 even when no data is imported.", E_USER_ERROR);

			$delTables[] = $tableName;
		}
		else
			trigger_error("Attribute 'from' is missing or invalid value in <Imports> section, table '" . $tableName . "'", E_USER_ERROR);
	}

	deleteTables($delTables);
	copyTables($copyTables);
}

function deleteTables($tableNames) {
	global $db3Name, $tablesInDb1;

	writeLog("DELETE TABLES:\n");

	for ($i = 0; $i < count($tableNames); $i++) {
		$tableName = $tableNames[$i];

		if (array_search($tableName, $tablesInDb1) !== FALSE)
			$rs = execSql("DELETE FROM " . $db3Name . "." . $tableName);
	}
}

function copyTables($tableNames) {
	global $db2Name, $db3Name, $tablesInDb1, $tablesInDb2;

	writeLog("COPY TABLES:\n");

	for ($i = 0; $i < count($tableNames); $i++) {
		$tableName = $tableNames[$i];

		if (array_search($tableName, $tablesInDb1) !== FALSE && array_search($tableName, $tablesInDb2) !== FALSE)
			$rs = execSql(
				"INSERT INTO " . $db3Name . "." . $tableName . " " .
				"SELECT * FROM " . $db2Name . "." . $tableName);
	}
}

function adjustSequences() {
	global $db3Name;

	writeLog("ADJUST SEQUENCES:\n");

	$rs = execSql(
		"SELECT name FROM " . $db3Name . ".sequences");

	while ($row = fetchNextRow($rs)) {
		$name = $row["name"];
		
		$pos = strrpos($name, '_');
		$tableName = substr($name, 0, $pos);
		$columnName = substr($name, $pos + 1, strlen($name) - ($pos + 1));

		// get the max value
		$rs2 = execSql("SELECT MAX(" . $columnName . ") AS max_value FROM " . $db3Name . "." . $tableName);

		$row2 = fetchOneRow($rs2);
		$max_value = $row2["max_value"];
		if ($max_value === null)
			$max_value = 0;

		// update the sequence value
		$rs2 = execSql(
			"UPDATE " . $db3Name . ".sequences " .
			"SET " . $db3Name . ".sequences.id = " . $max_value . " " .
			"WHERE " . $db3Name . ".sequences.name = '" . $name . "'");
	}
}

function importContentTypes() {
	global $xml, $db1Name, $db2Name, $typesInDb1, $typesInDb2, $oldNewMappings;

	$delContents = array(); // content-types whose contents will be deleted from db3
	$copyContents = array(); // content-types whose contents will be copied from db2 into db3

	$delComments = array(); // content-types whose comments will be deleted from db3
	$copyComments = array(); // content-types whose comments will be copied from db2 into db3

	$results = $xml->xpath("//Imports/Content-type");
	for ($i = 0; $i < count($results); $i++) {
		$typeName = (string)$results[$i]['name'];
		$contentFrom = (string)$results[$i]['contentFrom'];
		$commentsFrom = (string)$results[$i]['commentsFrom'];
		$mandatory = (string)$results[$i]['mandatory'];

		if ($mandatory == "true" || $mandatory == "false")
			$mandatory = ($mandatory == "true");
		else
			trigger_error("Attribute 'mandatory' is missing or invalid value in <Imports> section, content-type '" . $typeName . "'", E_USER_ERROR);

		// treatment for content
		if ($contentFrom == "db1") {
			if ($mandatory && array_search($typeName, $typesInDb1) === FALSE)
				trigger_error("Content-type '" . $typeName . "' specified in the <Imports> section does not exist in db1", E_USER_ERROR);

			// nothing to do (all content-types are initially copied from db1 when database is replicated into db3)
		}
		else if ($contentFrom == "db2") {
			if ($mandatory && array_search($typeName, $typesInDb2) === FALSE)
				trigger_error("Content-type '" . $typeName . "' specified in the <Imports> section does not exist in db2", E_USER_ERROR);
			
			if ($mandatory && array_search($typeName, $typesInDb1) === FALSE)
				trigger_error("Content-type '" . $typeName . "' specified in the <Imports> section does not exist in db1; table schema is always imported from db1 even when data is imported from db2.", E_USER_ERROR);
		
			$delContents[] = $typeName;
			$copyContents[] = $typeName;
		}
		else if ($contentFrom == "empty") {
			if ($mandatory && array_search($typeName, $typesInDb1) === FALSE)
				trigger_error("Content-type '" . $typeName . "' specified in the <Imports> section does not exist in db1; table schema is always imported from db1 even when no data is imported.", E_USER_ERROR);

			$delContents[] = $typeName;
		}
		else
			trigger_error("Attribute 'contentFrom' is missing or invalid value in <Imports> section, content-type '" . $typeName . "'", E_USER_ERROR);

		// treatment for comments
		if ($commentsFrom == "db1")
			trigger_error("Option 'db1' not implemented for comments in <Imports> section, content-type '" . $typeName . "'", E_USER_ERROR);
		else if ($commentsFrom == "db2") {
			if ($mandatory && array_search($typeName, $typesInDb2) === FALSE)
				trigger_error("Content-type '" . $typeName . "' specified in the <Imports> section does not exist in db2", E_USER_ERROR);
			
			if ($mandatory && array_search($typeName, $typesInDb1) === FALSE)
				trigger_error("Content-type '" . $typeName . "' specified in the <Imports> section does not exist in db1; table schema is always imported from db1 even when data is imported from db2.", E_USER_ERROR);
		
			$delComments[] = $typeName;
			$copyComments[] = $typeName;
		}
		else if ($commentsFrom == "empty") {
			if ($mandatory && array_search($typeName, $typesInDb1) === FALSE)
				trigger_error("Content-type '" . $typeName . "' specified in the <Imports> section does not exist in db1; table schema is always imported from db1 even when no data is imported.", E_USER_ERROR);

			$delComments[] = $typeName;
		}
		else
			trigger_error("Attribute 'contentFrom' is missing or invalid value in <Imports> section, content-type '" . $typeName . "'", E_USER_ERROR);
	}
	
	$delContentsAndComments = array_values(array_intersect($delContents, $delComments));
	$delContents = array_values(array_diff($delContents, $delContentsAndComments));
	$delComments = array_values(array_diff($delComments, $delContentsAndComments));

	$copyContentsAndComments = array_values(array_intersect($copyContents, $copyComments));
	$copyContents = array_values(array_diff($copyContents, $copyContentsAndComments));
	$copyComments = array_values(array_diff($copyComments, $copyContentsAndComments));

	$oldNewMappings = array();

	deleteTypes($delContents, ONLY_CONTENT);
	deleteTypes($delComments, ONLY_COMMENTS);
	deleteTypes($delContentsAndComments, CONTENT_AND_COMMENTS);
	copyTypes($copyContents, ONLY_CONTENT);
	copyTypes($copyComments, ONLY_COMMENTS);
	copyTypes($copyContentsAndComments, CONTENT_AND_COMMENTS);
}

function deleteTypes($typeNames, $op) {
	global $typesInDb1;

	for ($i = 0; $i < count($typeNames); $i++) {
		$typeName = $typeNames[$i];

		if (array_search($typeName, $typesInDb1) !== FALSE)
			deleteTypeTables($typeName, $op);
	}
}

function copyTypes($typeNames, $op) {
	global $typesInDb1, $typesInDb2;

	for ($i = 0; $i < count($typeNames); $i++) {
		$typeName = $typeNames[$i];

		if (array_search($typeName, $typesInDb1) !== FALSE && array_search($typeName, $typesInDb2) !== FALSE)
			copyTypeTables($typeName, $op);
	}
}

function replaceTables() {
	global $xml;

	$results = $xml->xpath("//Replacements/Table");
	for ($i = 0; $i < count($results); $i++) {
		$tableName = (string)$results[$i]['name'];

		$results2 = $xml->xpath("//Replacements/Table[@name = '" . $tableName . "']/Column");
		
		if (count($results2) > 0) {
			$replaces = array();

			for ($j = 0; $j < count($results2); $j++) {
				$colName = (string)$results2[$j]['name'];
				$colType = (string)$results2[$j]['type'];
				$colSearch = (string)$results2[$j]['search'];
				$colReplace = (string)$results2[$j]['replace'];

				$replaces[$colName] = array($colType, $colSearch, $colReplace);
			}
			
			replaceTable($tableName, $replaces);
		}
	}
}

function deleteTypeTables($typeName, $op) {
	global $db1Name, $db3Name;

	if ($op == ONLY_CONTENT) {
		writeLog("DELETE CONTENT (" . $typeName . "):\n");
		$tables = getContentTables($db3Name);
	}
	else if ($op == ONLY_COMMENTS) {
		writeLog("DELETE COMMENTS (" . $typeName . "):\n");
		$tables = getCommentTables($db3Name);
	}
	else {
		writeLog("DELETE CONTENT AND COMMENTS (" . $typeName . "):\n");
		$tables = array_values(array_merge(getContentTables($db3Name), getCommentTables($db3Name)));
	}

	for ($i = 0; $i < count($tables); $i++) {
		if ($tables[$i] == 'nodewords') {
			$sql =
				"DELETE FROM " . $db3Name . "." . $tables[$i] . " " .
				"USING " . $db3Name . "." . $tables[$i] . ", " . $db1Name . ".node " .
				"WHERE " . $db1Name . ".node.type = '" . sqlType($typeName) . "' " .
				"AND " . $db3Name . "." . $tables[$i] . ".id = " . $db1Name . ".node.nid " .
				"AND " . $db3Name . "." . $tables[$i] . ".type = 'node'";
		}
		else {
			$sql =
				"DELETE FROM " . $db3Name . "." . $tables[$i] . " " .
				"USING " . $db3Name . "." . $tables[$i] . ", " . $db1Name . ".node " .
				"WHERE " . $db1Name . ".node.type = '" . sqlType($typeName) . "' " .
				"AND " . $db3Name . "." . $tables[$i] . ".nid = " . $db1Name . ".node.nid";
		}
		$rs = execSql($sql);
	}
}

function copyTypeTables($typeName, $op) {
	global $db2Name, $db3Name, $oldNewMappings;

	if ($op == ONLY_CONTENT) {
		writeLog("COPY CONTENT (" . $typeName . "):\n");
		$tables = getContentTables($db3Name);
	}
	else if ($op == ONLY_COMMENTS) {
		writeLog("COPY COMMENTS (" . $typeName . "):\n");
		$tables = getCommentTables($db3Name);
	}
	else {
		writeLog("COPY CONTENT AND COMMENTS (" . $typeName . "):\n");
		$tables = array_values(array_merge(getContentTables($db3Name), getCommentTables($db3Name)));
	}

	$rs = execSql(
		"SELECT MAX(nid) AS maxNid " .
		"FROM " . $db3Name . ".node");

	$row = fetchOneRow($rs);
	$nextNid = $row["maxNid"] + 1;

	freeResult($rs);

	$rs = execSql(
		"SELECT nid " .
		"FROM " . $db2Name . ".node " .
		"WHERE " . $db2Name . ".node.type = '" . sqlType($typeName) . "' " .
		"ORDER BY nid");

	while ($row = fetchNextRow($rs)) {
		$oldNid = $row["nid"];

		if ($op == ONLY_COMMENTS) {
			$newNid = $oldNid;
		}
		else {
			$newNid = $nextNid;
			$nextNid += 1;
		}

		// copy all related content-type tables
		for ($i = 0; $i < count($tables); $i++) {
			copyRow($tables[$i], $oldNid, $newNid);
			$oldNewMappings[$oldNid] = $newNid;
		}
	}

	freeResult($rs);
}

function copyRow($tableName, $oldNid, $newNid) {
	global $db2Name, $db3Name;

	if ($tableName == 'nodewords') {
		$sql =
			"SELECT * FROM " . $db2Name . "." . $tableName . " " .
			"WHERE " . $db2Name . "." . $tableName . ".id = " . $oldNid . " " .
			"AND " . $db2Name . "." . $tableName . ".type = 'node'";
	}
	else {
		$sql =
			"SELECT * FROM " . $db2Name . "." . $tableName . " " .
			"WHERE " . $db2Name . "." . $tableName . ".nid = " . $oldNid;
	}
	$rs = execSql($sql);

	$row = fetchNextRow($rs);
	freeResult($rs);
	
	if (! $row)
		return;

	$selCols = "";
	$cols = array_keys($row);
	while ($colName = array_shift($cols)) {
		if (strlen($selCols) > 0)
			$selCols .= ", ";

		if ($tableName == 'nodewords') {
			if ($colName == "id")
				$selCols .= $newNid;
			else
				$selCols .= $colName;
		}
		else {
			if ($colName == "nid" || $colName == "vid")
				$selCols .= $newNid;
			else
				$selCols .= $colName;
		}
	}

	if ($tableName == 'nodewords') {
		$sql =
			"INSERT INTO " . $db3Name . "." . $tableName . " " .
			"SELECT " . $selCols . " FROM " . $db2Name . "." . $tableName . " " .
			"WHERE " . $db2Name . "." . $tableName . ".id = " . $oldNid . " " .
			"AND " . $db2Name . "." . $tableName . ".type = 'node'";
	}
	else {
		$sql =
			"INSERT INTO " . $db3Name . "." . $tableName . " " .
			"SELECT " . $selCols . " FROM " . $db2Name . "." . $tableName . " " .
			"WHERE " . $db2Name . "." . $tableName . ".nid = " . $oldNid;
	}
	$rs = execSql($sql);
}

function replaceTable($tableName, $replaces) {
	global $db3Name;

	writeLog("REPLACE TABLE (" . $tableName . "):\n");

	$sql =
		"SELECT * FROM " . $db3Name . "." . $tableName;
	$rs = execSql($sql);

	while ($row = fetchNextRow($rs)) {
		$nid = $row["nid"];
		$delta = $row["delta"];

		replaceRow($tableName, $row, $nid, $delta, $replaces);
	}

	freeResult($rs);
}

function replaceRow($tableName, $row, $nid, $delta, $replaces) {
	global $conn, $db3Name, $oldNewMappings;

	$updateCols = "";
	reset($row);
	while (list($colName, $colValue) = each($row)) {
		if (array_key_exists($colName, $replaces)) {
			$colType = $replaces[$colName][0];
			$colSearch = $replaces[$colName][1];
			$colReplace = $replaces[$colName][2];
			
			$languages = array('en', 'es');
			for ($i = 0; $i < count($languages); $i++) {
				$s = str_replace("\${LANG}", $languages[$i], $colSearch);

				preg_match($s, $colValue, $matches);
				if ($matches[1] !== null) {
					$colSearch = $s;
					break;
				}
			}
			if ($i == count($languages))
				trigger_error("Search '" . $colSearch . "' did not produce the expected OLD_NID integer value required for replacement in <Replacements> section, table '" . $tableName . "', column '" . $colName . "'", E_USER_ERROR);

			if ($oldNewMappings[$matches[1]] === null)
				trigger_error("OLD_NID " . $matches[1] . " was not found in memory during the replacement '" . $colReplace . "' in <Replacements> section, table '" . $tableName . "', column '" . $colName . "'", E_USER_ERROR);

			$colReplace = str_replace("\${NEW_NID}", $oldNewMappings[$matches[1]], $colReplace);
			$newValue = preg_replace($colSearch, $colReplace, $colValue);
			$newValue = mysql_real_escape_string($newValue, $conn);

			if ($colType != "int")
				$newValue = "'" . $newValue . "'";
			
			if (strlen($updateCols) > 0)
				$updateCols .= ", ";

			$updateCols .= $colName . " = " . $newValue;
		}
	}

	$sql =
		"UPDATE " . $db3Name . "." . $tableName . " " .
		"SET " . $updateCols . " " .
		"WHERE " . $db3Name . "." . $tableName . ".nid = " . $nid;
	if ($delta !== null)
		$sql .= " AND " . $db3Name . "." . $tableName . ".delta = " . $delta;
	$rs = execSql($sql);
}

function deleteDatabase($dbName) {
	writeLog("DROP DATABASE:\n");

	$rs = execSql("DROP DATABASE IF EXISTS " . $dbName);
}

function copyDatabase($sourceName, $targetName) {
	global $xml;

	writeLog("COPY DATABASE:\n");

	$dbDir = $xml->DBServer->DBDir;
	if (! $dbDir || ! file_exists($dbDir))
		trigger_error("Database directory not found '" . $dbDir . "'", E_USER_ERROR);

	$rs = execSql("DROP DATABASE IF EXISTS " . $targetName);

	mkdir($dbDir . '/' . $targetName);
	$files = scandir($dbDir . '/' . $sourceName);
	for ($i = 0; $i < count($files); $i++) {
		if ($files[$i] != '.' && $files[$i] != '..')
			copy($dbDir . '/' . $sourceName . '/' . $files[$i], $dbDir . '/' . $targetName . '/' . $files[$i]);
	}
}

function writeLog($msg) {
	global $logHandle;

	if ($logHandle) {
		fwrite($logHandle, $msg);
		fflush($logHandle);
	}
}

function sqlType($typeName) {
	$types = getPredefinedTypes();

	if (array_search($typeName, $types) === FALSE)
		return "content_" . $typeName;
	else
		return $typeName;
}

function getTypesInDb($dbName) {
	$tables = listTables($dbName);
	
	$types = getPredefinedTypes();

	for ($i = 0; $i < count($tables); $i++) {
		if (strpos($tables[$i], "node_content_") === 0)
			$types[] = substr($tables[$i], strlen("node_content_"));
	}

	return $types;
}

function getTablesInDb($dbName) {
	global $xml;

	$tables = listTables($dbName);
	$discardTables = getContentTables($dbName);
	$discardTables = array_merge($discardTables, getCommentTables($dbName));
	
	$diff = array_diff($discardTables, $tables);
	if (count($diff) > 0)
		trigger_error("The following tables specified in the <Content-type-tables> section were not found in " . $dbName . " database: '" . arrayToString($diff) . "'", E_USER_ERROR);

	return array_values(array_diff($tables, $discardTables)); // the call to array_values forces index recalculation (as it creates a new array)
}

function getContentTables($dbName) {
	global $xml;

	$results = $xml->xpath("//Content-type-tables/Table[@type='content']");

	$typeTables = array();
	for ($i = 0; $i < count($results); $i++) {
		$tableName = (string)$results[$i]['name'];
		if (substr($tableName, strlen($tableName) - 1, 1) == "*") {
			$expandTables = expandWildcard($dbName, $tableName);
			$typeTables = array_merge($typeTables, $expandTables);
		}
		else {
			$typeTables[] = $tableName;
		}
	}
	
	return array_values($typeTables); // the call to array_values forces index recalculation (as it creates a new array)
}

function getCommentTables($dbName) {
	global $xml;

	$results = $xml->xpath("//Content-type-tables/Table[@type='comments']");

	$typeTables = array();
	for ($i = 0; $i < count($results); $i++) {
		$tableName = (string)$results[$i]['name'];
		if (substr($tableName, strlen($tableName) - 1, 1) == "*") {
			$expandTables = expandWildcard($dbName, $tableName);
			$typeTables = array_merge($typeTables, $expandTables);
		}
		else {
			$typeTables[] = $tableName;
		}
	}
	
	return array_values($typeTables); // the call to array_values forces index recalculation (as it creates a new array)
}

function getTypesInXml($xml) {
	$types = array();

	$results = $xml->xpath("//Imports/Content-type");
	for ($i = 0; $i < count($results); $i++) {
		$typeName = (string)$results[$i]['name'];

		// check for duplicated content-types in the XML file
		if (array_search($typeName, $types) === FALSE)
			$types[] = $typeName;
		else
			trigger_error("Duplicated content-type '" . $typeName . "' in <Imports> section", E_USER_ERROR);
	}
	
	return $types;
}

function getTablesInXml($xml) {
	$tables = array();

	$results = $xml->xpath("//Imports/Table");
	for ($i = 0; $i < count($results); $i++) {
		$tableName = (string)$results[$i]['name'];

		// check for duplicated tables in the XML file
		if (array_search($tableName, $tables) === FALSE)
			$tables[] = $tableName;
		else
			trigger_error("Duplicated table '" . $tableName . "' in <Imports> section", E_USER_ERROR);
	}

	return $tables;
}

function expandWildcard($dbName, $tableName) {
	$tables = listTables($dbName);
	$prefix = substr($tableName, 0, strlen($tableName) - 1);

	$expandTables = array();
	for ($i = 0; $i < count($tables); $i++) {
		if (strpos($tables[$i], $prefix) === 0)
			$expandTables[] = $tables[$i];
	}
	
	return $expandTables;
}

function arrayToString($arr, $separator = " ", $indent = "") {
	if ($value = array_shift($arr))
		$s = $indent . $value;
	else
		$s = "";
	
	while ($value = array_shift($arr)) {
		$s = $s . $separator . $indent . $value;
	}
	
	return $s;
}

function connect() {
	global $xml;

	$conn = mysql_connect($xml->DBServer->Host, $xml->DBServer->User, $xml->DBServer->Passwd);
	if (! $conn)
		trigger_error(mysql_error(), E_USER_ERROR);

	return $conn;
}

function disconnect($conn) {
	mysql_close($conn);
}

function execSql($sql) {
	writeLog("	" . $sql . "\n");

	$rs = mysql_query($sql);
	if (! $rs)
		trigger_error(mysql_error(), E_USER_ERROR);
		
	return $rs;
}

function fetchOneRow($rs) {
	$row = mysql_fetch_assoc($rs);
	if (! $row)
		trigger_error("SELECT statement did not return any rows", E_USER_ERROR);

	return $row;
}

function fetchNextRow($rs) {
	$row = mysql_fetch_assoc($rs);

	return $row;
}

function freeResult($rs) {
	if (! mysql_free_result($rs))
		trigger_error(mysql_error(), E_USER_ERROR);
}

function listTables($dbName) {
	$rs = mysql_list_tables($dbName);
	if (! $rs)
		trigger_error(mysql_error(), E_USER_ERROR);

	$tables = array();
	while ($row = mysql_fetch_row($rs))
		$tables[] = $row[0];

	freeResult($rs);
	return $tables;
}

function formatTime($seconds) {
	$hours = floor($seconds / 3600 );
	$seconds = ($seconds / 3600 - $hours) * 3600;
	$minutes = floor($seconds / 60);
	$seconds = ($seconds / 60 - $minutes) * 60;

	if ($minutes < 10)
		$minutes = "0" . $minutes;

	if ($seconds < 10)
		$seconds = "0" . $seconds;
	
	return $hours . ":" . $minutes . ":" . $seconds;
}

function getPredefinedTypes() {
	return array("forum", "page", "story");
}

function main() {
	global $xml, $conn, $db1Name, $db2Name, $db3Name, $logHandle;

	// load config file
	$xml = simplexml_load_file("mergedb.xml");

	// open log file
	if ($xml->LogFile)
		$logHandle = fopen($xml->LogFile, "w");
	else
		$logHandle = null;

	$startTime = time();
	writeLog("MergeDB process started at " . strftime("%b %d %Y %H:%M:%S", $startTime) . "\n");

	// get database names
	$results = $xml->xpath("//Database[@aliasName='db1']");
	$db1Name = $results[0]['realName'];
	$results = $xml->xpath("//Database[@aliasName='db2']");
	$db2Name = $results[0]['realName'];
	$results = $xml->xpath("//Database[@aliasName='db3']");
	$db3Name = $results[0]['realName'];

	// ask for confirmation
	echo "WARNING! The following database will be deleted:\n";
	echo "  " . $db3Name . "\n";
	echo "Do you wish to continue (y/n)?";
	if (trim(fgets(STDIN)) != "y")
		trigger_error("User cancelled", E_USER_ERROR);

	$conn = connect();
	checkXml();
	checkNotSupported($db1Name);
	checkNotSupported($db2Name);
	checkNodeConsistency();
	checkForumConsistency();
	copyDatabase($db1Name, $db3Name);
	importTables();
	importContentTypes();
	replaceTables();
	adjustSequences();
	disconnect($conn);

	$finishTime = time();
	writeLog("MergeDB process finished at " . strftime("%b %d %Y %H:%M:%S", $finishTime) . "\n");
	writeLog("Elapsed: " . formatTime($finishTime - $startTime) . "\n");

	// close log file
	if ($xml->LogFile)
		fclose($logHandle);
}

main();

?>
