<?php

namespace Rhymix\Framework\Parsers;

/**
 * DB table parser class for XE compatibility.
 */
class DBTableParser extends BaseParser
{
	/**
	 * Mapping for XE-compatible types.
	 */
	protected const XE_COMPAT_TYPES = array(
		'bignumber' => 'bigint',
		'number' => 'bigint',
		'bigtext' => 'longtext',
		'date' => 'char(14)',
	);

	/**
	 * List of types for which the size attribute will be ignored.
	 */
	protected const NO_SIZE_TYPES = array(
		'bigint' => true,
		'int' => true,
		'integer' => true,
	);

	/**
	 * Load a table definition XML file.
	 *
	 * @param string $filename
	 * @param string $content
	 * @return ?object
	 */
	public static function loadXML(string $filename = '', string $content = ''): ?object
	{
		// Load the XML content.
		if ($content)
		{
			$xml = simplexml_load_string($content);
		}
		else
		{
			$xml = simplexml_load_string(file_get_contents($filename));
		}

		if ($xml === false)
		{
			return null;
		}

		// Initialize table definition.
		$table = new DBTable\Table;
		if ($filename)
		{
			$table->name = preg_replace('/\.xml$/', '', basename($filename));
		}
		else
		{
			$table->name = strval($xml['name']);
		}

		$is_deleted = strval($xml['deleted']);
		if ($is_deleted !== '')
		{
			$table->is_deleted = toBool($is_deleted);
		}

		// Load columns.
		foreach ($xml->column as $column_info)
		{
			// Is this column generated?
			$is_generated = strval($column_info['generated'] ?? '') !== '';
			if ($is_generated)
			{
				$column = new DBTable\GeneratedColumn;
				$column->generated = strtolower($column_info['generated']);
				$column->is_stored = strtolower($column_info['stored'] ?? '');
				$column->is_stored = $column->is_stored !== 'virtual' && toBool($column->is_stored);
			}
			else
			{
				$column = new DBTable\Column;
			}

			// Get the column name and type.
			$column->name = strval($column_info['name']);
			list($column->type, $column->xetype, $column->size) = self::getTypeAndSize(strval($column_info['type']), strval($column_info['size']));

			// Get all attributes.
			$attribs = self::_getAttributes($column_info);

			// Get the charset/utf8mb4 attribute.
			if (isset($attribs['charset']))
			{
				$column->charset = $attribs['charset'];
			}
			elseif (isset($attribs['utf8mb4']))
			{
				$column->charset = toBool($attribs['utf8mb4']) ? 'utf8mb4' : 'utf8';
			}
			elseif ($column->xetype === 'date' || ($column->name === 'ipaddress' && $column->size >= 60) || ($column->type === 'char' && $column->size == 1))
			{
				$column->charset = 'latin1';
			}

			// Get the default value.
			if (isset($attribs['default']))
			{
				$column->default_value = $attribs['default'];
			}

			// Get the NOT NULL attribute.
			if (isset($attribs['notnull']))
			{
				$column->not_null = true;
			}

			// Get index information.
			if (isset($attribs['index']))
			{
				$index_name = $attribs['index'];
				if (!isset($table->indexes[$index_name]))
				{
					$table->indexes[$index_name] = new DBTable\Index;
					$table->indexes[$index_name]->name = $index_name;
				}
				$table->indexes[$index_name]->columns[$column->name] = 0;
				$column->is_indexed = true;
			}
			if (isset($attribs['unique']))
			{
				$index_name = $attribs['unique'];
				if (!isset($table->indexes[$index_name]))
				{
					$table->indexes[$index_name] = new DBTable\Index;
					$table->indexes[$index_name]->name = $index_name;
					$table->indexes[$index_name]->type = 'UNIQUE';
				}
				$table->indexes[$index_name]->columns[$column->name] = 0;
				$column->is_indexed = true;
				$column->is_unique = true;
			}

			// Get primary key information.
			if (isset($attribs['primarykey']) && toBool($attribs['primarykey']))
			{
				$table->primary_key[] = $column->name;
				$column->is_indexed = true;
				$column->is_unique = true;
				$column->is_primary_key = true;
			}

			// Get auto-increment information.
			if (isset($attribs['autoincrement']) && toBool($attribs['autoincrement']))
			{
				$column->auto_increment = true;
			}

			// Add the column to the table definition.
			$table->columns[$column->name] = $column;
		}

		// Load indexes.
		foreach ($xml->index as $index_info)
		{
			// Get the index name and list of columns.
			$index_info = self::_getAttributes($index_info);
			$index = new DBTable\Index;
			$index->name = $index_info['name'];
			$idxcolumns = array_map('trim', explode(',', $index_info['columns'] ?? $index_info['column']));
			foreach ($idxcolumns as $idxcolumn)
			{
				if (preg_match('/^(\S+)\s*\(([0-9]+)\)$/', $idxcolumn, $matches))
				{
					$index->columns[$matches[1]] = intval($matches[2]);
					$idxcolumn = $matches[1];
				}
				else
				{
					$index->columns[$idxcolumn] = 0;
				}
			}

			// Get the index type.
			if (isset($index_info['type']) && $index_info['type'])
			{
				$index->type = strtoupper($index_info['type']);
			}
			elseif (isset($index_info['unique']) && toBool($index_info['unique']))
			{
				$index->type = 'UNIQUE';
			}

			// Set attributes on indexed columns.
			if (isset($table->columns[$idxcolumn]) && is_object($table->columns[$idxcolumn]))
			{
				$table->columns[$idxcolumn]->is_indexed = true;
				$table->columns[$idxcolumn]->is_unique = $index->type === 'UNIQUE' ? true : $table->columns[$idxcolumn]->is_unique;
			}

			// If any index options are given, also store them in the index class.
			if (isset($index_info['options']) && $index_info['options'])
			{
				$index->options = $index_info['options'];
			}

			// Add the index to the column definition.
			$table->indexes[$index->name] = $index;
		}

		// Load other constraints (foreign keys).
		foreach ($xml->constraint as $const_info)
		{
			$const_info = self::_getAttributes($const_info);
			$constraint = new DBTable\Constraint;
			$constraint->type = strtoupper($const_info['type'] ?? '');
			$constraint->column = ($const_info['column'] ?? null) ?: null;
			$constraint->references = ($const_info['references'] ?? null) ?: null;
			$constraint->condition = ($const_info['condition'] ?? null) ?: null;
			$constraint->on_delete = ($const_info['ondelete'] ?? null) ?: $constraint->on_delete;
			$constraint->on_update = ($const_info['onupdate'] ?? null) ?: $constraint->on_update;
			$table->constraints[] = $constraint;
		}

		// Return the complete table definition.
		return $table;
	}

	/**
	 * Get column type and size.
	 *
	 * @param string $type
	 * @param string $size
	 * @return array
	 */
	public static function getTypeAndSize(string $type, string $size): array
	{
		// Map XE-compatible types to database native types.
		if (isset(self::XE_COMPAT_TYPES[$type]))
		{
			$xetype = $type;
			$type = self::XE_COMPAT_TYPES[$type];
		}
		else
		{
			$xetype = 'none';
			$type = ltrim($type, '\\');
		}

		// Extract and normalize the size.
		if (preg_match('/^([a-z0-9_]+)\(([0-9,\s]+)\)$/i', $type, $matches))
		{
			$type = $matches[1];
			$size = $matches[2];
		}
		$size = implode(',', array_map('trim', explode(',', $size))) ?: null;
		if (isset(self::NO_SIZE_TYPES[$type]))
		{
			$size = null;
		}

		// Return a complete array.
		return [$type, $xetype, $size];
	}

	/**
	 * Get the XE-compatible type from a real database type.
	 *
	 * @param string $type
	 * @param string $size
	 * @return string
	 */
	public static function getXEType(string $type, string $size): string
	{
		$type = strtolower($type);
		switch ($type)
		{
			case 'bigint':
				return 'bignumber';
			case 'int':
			case 'integer':
				return 'number';
			case 'longtext':
				return 'bigtext';
			case 'char':
			case 'varchar':
				if ($size == 14)
				{
					return 'date';
				}
			default:
				return $type;
		}
	}

	/**
	 * Order tables according to foreign key relations.
	 *
	 * @param array $tables [$table_name => $filename]
	 * @return array
	 */
	public static function resolveDependency(array $tables): array
	{
		// Compile the list of each table's dependency.
		$ref_list = [];
		$i = 0;
		foreach ($tables as $table_name => $filename)
		{
			$table = self::loadXML($filename);
			if ($table)
			{
				$info = (object)['name' => $table_name, 'refs' => [], 'index' => $i++];
				foreach ($table->constraints as $constraint)
				{
					if ($constraint->references)
					{
						$ref = explode('.', $constraint->references);
						$reference_table_name = $ref[0];
						if ($reference_table_name === $table_name)
						{
							continue; // Ignore self-references.
						}

						$info->refs[] = $reference_table_name;
					}
				}
				$ref_list[$table_name] = $info;
			}
		}

		// Sort each table after the ones they are dependent on.
		for ($j = 0; $j < count($ref_list); $j++)
		{
			$changed = false;
			foreach ($ref_list as $table_name => $info)
			{
				if (count($info->refs))
				{
					foreach ($info->refs as $ref_name)
					{
						if (isset($ref_list[$ref_name]) && $info->index <= $ref_list[$ref_name]->index)
						{
							$info->index = $ref_list[$ref_name]->index + 1;
							$changed = true;
						}
					}
				}
			}
			if (!$changed)
			{
				break;
			}
		}

		uasort($ref_list, function($a, $b) {
			return $a->index - $b->index;
		});

		// Produce a result in the same format as the input.
		$result = [];
		foreach ($ref_list as $table_name => $info)
		{
			$result[$table_name] = $tables[$table_name];
		}
		return $result;
	}
}
