[self::MOST_PREFERRED_STORAGE_ENGINE, "Aria", "Maria", "MyISAM"],
		"Aria"   => ["Aria", "Maria", "MyISAM"],
		"InnoDB" => ["InnoDB", "XtraDB"],
		"XtraDB" => ["XtraDB", "InnoDB"],
	];
	
	var $fieldTypes = array('time','timestamp','datetime','year','tinyblob','blob',
							'mediumblob','longblob','tinytext','mediumtext','longtext','text','date', 'json');
							
	var $fieldTypeNum = array('bit','tinyint','smallint','mediumint','integer','int','bigint',
		'real','double','float','decimal','numeric','varchar','char','binary','varbinary','enum','set');
	const STATUS_TABLE_OK = 0x0;
	const STATUS_TABLE_MISSING = 0x1 << 1;
	const STATUS_TABLE_MISMATCH_STORAGE_ENGINE = 0x1 << 2;
	const STATUS_TABLE_MISMATCH_DEFAULT_CHARSET = 0x1 << 3;
	var $modes = array(
			'missing_table'		=> 'create',
			'mismatch' 			=> 'alter',
			'missing_field'		=> 'insert',
			'missing_index' 	=> 'index',
			'mismatch_index' 	=> '', // TODO
		);
	
	var $errors = array();
	const cachetag = 'Dbverify';
	/**
	 * Setup
	 */
	function __construct()
	{
				
		$sql = e107::getDb();
		$sql->gen('SET SQL_QUOTE_SHOW_CREATE = 1');
		$this->backUrl = e_SELF;
		if(!deftrue('e_DEBUG') && $tmp = e107::getCache()->retrieve(self::cachetag, 15, true, true))
		{
			$cacheData = e107::unserialize($tmp);
			$this->sqlFileTables = isset($cacheData['sqlFileTables']) ? $cacheData['sqlFileTables'] : $this->load();
			$this->availableStorageEngines = isset($cacheData['availableStorageEngines']) ?
				$cacheData['availableStorageEngines'] : $this->getAvailableStorageEngines();
		}
		else
		{
			$this->sqlFileTables = $this->load();
			$this->availableStorageEngines = $this->getAvailableStorageEngines();
			$cacheData = e107::serialize([
				'sqlFileTables' => $this->sqlFileTables,
				'availableStorageEngines' => $this->availableStorageEngines,
			], 'json');
			e107::getCache()->set(self::cachetag, $cacheData, true, true, true);
		}
		$this->sqlLanguageTables = $this->getSqlLanguages();
	//	$this->loadCreateTableData();
		return $this;
		
	}
	/**
	 * @return bool
	 */
	public function clearCache()
	{
		return e107::getCache()->clear(self::cachetag, true);
	}
	/**
	 * @return array
	 */
	private function load()
	{
		$mes = e107::getMessage();
		$pref = e107::getPref();
		$ret = array();
		$core_data = file_get_contents(e_CORE.'sql/core_sql.php');
		$ret['core'] = $this->getSqlFileTables($core_data);
		if(!empty($pref['e_sql_list']))
		{
			foreach($pref['e_sql_list'] as $path => $file)
			{
				$filename = e_PLUGIN.$path.'/'.$file.'.php';
				if(is_readable($filename))
				{
					$id = str_replace('_sql','',$file);
					$data = file_get_contents($filename);
					$this->currentTable = $id;
					$ret[$id] = $this->getSqlFileTables($data);
			      	unset($data);
				}
				else
				{
					$message = str_replace("[x]",$filename,DBVLAN_22);
			      	$mes->add($message, E_MESSAGE_WARNING);
				}
			}
		}
		return $ret;
	}
	/**
	 * Permissive field validation
	 */
	private function diffStructurePermissive($expected, $actual)
	{
		$expected['default'] = isset($expected['default']) ? $expected['default'] : '';
		$actual['default']   = isset($actual['default'])   ? $actual['default']   : '';
		if($expected['type'] === 'JSON' && $actual['type'] !== 'JSON') // Fix for JSON alias MySQL 5.7+
		{
			$expected['type'] = 'LONGTEXT';
		}
		// Permit actual text types that default to null even when
		// expected does not explicitly default to null
		if(0 === strcasecmp($expected['type'], $actual['type']) &&
		   1 === preg_match('/[A-Z]*TEXT/i', $expected['type']) &&
		   0 === strcasecmp($actual['default'], "DEFAULT NULL"))
		{
			$expected['default'] = $actual['default'];
		}
		// Loosely typed default value for numeric types
		if(1 === preg_match('/([A-Z]*INT|NUMERIC|DEC|FIXED|FLOAT|REAL|DOUBLE)/i', $expected['type']))
		{
			$expected['default'] = preg_replace("/DEFAULT '(\d*\.?\d*)'/i", 'DEFAULT $1', $expected['default']);
			$actual['default']   = preg_replace("/DEFAULT '(\d*\.?\d*)'/i", 'DEFAULT $1', $actual['default']  );
		}
		/**
		 * Display width specification for integer data types was deprecated in MySQL 8.0.17
		 * @see https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-19.html
		 */
		if(1 === preg_match('/([A-Z]*INT)/i', $expected['type']))
		{
			$expected['value'] = '';
			$actual['value'] = '';
		}
		// Correct difference on CREATE TABLE statement between MariaDB and MySQL
		if(1 === preg_match('/(DATE|DATETIME|TIMESTAMP|TIME|YEAR)/i', $expected['default']))
		{
			$expected['default'] = preg_replace("/CURRENT_TIMESTAMP\(\)/i", 'CURRENT_TIMESTAMP', $expected['default']);
			$actual['default']   = preg_replace("/CURRENT_TIMESTAMP\(\)/i", 'CURRENT_TIMESTAMP', $actual['default']  );
		}
		return array_diff_assoc($expected, $actual);
	}
	
	/**
	 * Main Routine for checking and rendering results. 
	 */
	function verify()
	{
		if(!empty($_POST['runfix']))
		{
			$this->runFix($_POST['fix']);
		}
		if(!empty($_POST['verify_table']))
		{			
			$this->runComparison($_POST['verify_table']);
		}
		else
		{
			$this->renderTableSelect();
		}	
	}
	/**
	 * @param $fileArray
	 * @return void
	 */
	function runComparison($fileArray)
	{
		$mes = e107::getMessage();
		foreach($fileArray as $tab)
		{			
			$this->compare($tab);	
			foreach($this->sqlLanguageTables as $lng=>$lantab)
			{
				$this->compare($tab,$lng);
			}			
		}
				
		if($cnt = $this->errors())
		{
			$message = str_replace("[x]",$cnt,DBVLAN_26); // Found [x] issues.
			$mes->add($message, E_MESSAGE_WARNING); 
			$this->renderResults($fileArray);
		}
		else
		{
			if($this->internalError === false)
			{
				$mes->addSuccess(DBLAN_111);
				$mes->addSuccess("".LAN_BACK."");
			}
			//$debug = "
".print_r($this->results,TRUE)."
";
			//$mes->add($debug,E_MESSAGE_DEBUG);	
			//$text .= "".$frm->admin_button('back', DBVLAN_17, 'back')."
";
			echo $mes->render();
		//	$ns->tablerender("Okay",$mes->render().$text);
		}	
			
	}	
		
	//	$this->sqlTables = $this->sqlTableList();
		
	//	print_a($this->tables);
		// $this->renderTableSelect();
			
	//	print_a($field);
	//	print_a($match[2]);
		// echo "".$sql_data."
";
	
	/**
	 * Check core tables and installed plugin tables
	 * @param $exclude - array of plugins to exclude. 
	 */
	function compareAll($exclude = null)
	{
		
		if(is_array($exclude))
		{
			foreach($exclude as $val)
			{
				unset($this->sqlFileTables[$val]);
			}
		}
		
		$dtables = array_keys($this->sqlFileTables);
		foreach($dtables as $tb)
		{
			$this->compare($tb);	
		}
			
		if(!empty($this->sqlLanguageTables)) // language tables. 
		{
			foreach($this->sqlLanguageTables as $lng=>$lantab) 
			{
				foreach($dtables as $tb)
				{
					$this->compare($tb,$lng);	
				}			
			}
		}
	}
	/**
	 * @param $selection
	 * @param $language
	 * @return false|void
	 */
	public function compare($selection, $language='')
	{
		$this->currentTable = $selection;
	//	var_dump($this->sqlFileTables[$selection]);
		if(!isset($this->sqlFileTables[$selection])) // doesn't have an SQL file.
		{
		// e107::getMessage()->addDebug("No SQL File for ".$selection);
			return false;
		}
		if(empty($this->sqlFileTables[$selection]['tables']))
		{
			//$this->internalError = true;
			e107::getMessage()->addDebug("Couldn't read table data for ".$selection);
			return false;
		}
		foreach($this->sqlFileTables[$selection]['tables'] as $key=>$tbl)
		{
			$rawSqlData = $this->getSqlData($tbl,$language);
			
			
			if($rawSqlData === false)
			{
				if($language) continue;
				
				
				
				$this->errors[$tbl]['_status'] = self::STATUS_TABLE_MISSING;
				$this->errors[$tbl]['_file'] = $selection;
				$this->results[$tbl] = [];
				// echo "missing table: $tbl";
				continue;
			}
		//	echo "RAW
";
		//	print_a($rawSqlData);
					//	$this->currentTable = $tbl;v
			$sqlDataArr     = $this->getSqlFileTables($rawSqlData);
		//	echo "PARSED
";
		//	print_a($sqlDataArr);
			$fileData['field']	= $this->getFields($this->sqlFileTables[$selection]['data'][$key]);
			$sqlData['field']	= $this->getFields($sqlDataArr['data'][0]);
			
			$fileData['index']	= $this->getIndex($this->sqlFileTables[$selection]['data'][$key]);
			$sqlData['index']	= $this->getIndex($sqlDataArr['data'][0]);
			$maybeEngine = isset($sqlDataArr['engine'][0]) ? $sqlDataArr['engine'][0] : 'INTERNAL_ERROR:ENGINE';
			$fileData['engine'] = $this->getIntendedStorageEngine($this->sqlFileTables[$selection]['engine'][$key]);
			$sqlData['engine'] = $this->getCanonicalStorageEngine($maybeEngine);
			$maybeCharset = isset($sqlDataArr['charset'][0]) ? $sqlDataArr['charset'][0] : 'INTERNAL_ERROR:CHARSET';
			$fileData['charset'] = $this->getIntendedCharset($this->sqlFileTables[$selection]['charset'][$key]);
			$sqlData['charset'] = $this->getCanonicalCharset($maybeCharset);
		/*		
			$debugA = print_r($fileFieldData,TRUE);	// Extracted Field Arrays	
			$debugA .= "Index
";
			$debugA .= print_r($fileIndexData,TRUE);
			$debugB = print_r($sqlFieldData,TRUE); // Extracted Field Arrays	
			$debugB .= "Index
";
			$debugB .= print_r($sqlIndexData,TRUE);
		*/
		
			$debugA = $this->sqlFileTables[$selection]['data'][$key];	// Extracted Raw Field Text
		//	$debugB = $rawSqlData;
			$debugB = $sqlDataArr['data'][0];	// Extracted Raw Field Text	
			
			if(isset($debugA) && (e_PAGE === 'db.php'))
			{
									
				$debug = "
				| FILE: ".$tbl." (key=".$key.") | SQL: ".$tbl." | 
				| ".$debugA." | ".$debugB." | 
";
				  		
				$mes = e107::getMessage();
				$mes->add($debug,E_MESSAGE_DEBUG);
			}
			
			if($language)
			{
			 	$tbl = "lan_".$language."_".$tbl;
			}
			
			$this->prepareResults($tbl, $selection, $sqlData, $fileData);
			unset($data);
			
		}
		
	}
	/**
	 * @param string $type fields|indices
	 * @return array
	 */
	public function getResults($type='fields')
	{
		if($type === 'indices')
		{
			return $this->indices;
		}
		return $this->results;
	}
	/**
	 * @param string $tbl table name without prefix.
	 * @param string $selection 'core' OR plugin-folder name.
	 * @param array $sqlData ie. array('field'=>getFields($data), 'index'=>getFields($data));
	 * @param array $fileData ie. array('field'=>getFields($data), 'index'=>getFields($data));
	 * @todo Check for additional fields in SQL that should be removed.
	 * @todo Add support for MYSQL 5 table layout .eg. journal_id INT( 10 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
	 */
	public function prepareResults($tbl, $selection, $sqlData, $fileData)
	{
				// Check field and index data
		foreach(['field', 'index'] as $type)
		{
			$results = 'results';
			if ($type === 'index')
			{
				 $results = 'indices';
			}
			if (!isset($this->errors[$tbl])) $this->errors[$tbl] = [];
			if (!isset($this->errors[$tbl]['_status'])) $this->errors[$tbl]['_status'] = self::STATUS_TABLE_OK;
			$this->errors[$tbl]['_file'] = $selection;
			foreach($fileData[$type] as $key => $value)
			{
				$this->{$results}[$tbl][$key]['_status'] = 'ok';
				if(!isset($sqlData[$type][$key]) || !is_array($sqlData[$type][$key]))
				{
					$this->{$results}[$tbl][$key]['_status'] = "missing_$type"; // type status
					$this->{$results}[$tbl][$key]['_valid'] = $value;
					$this->{$results}[$tbl][$key]['_file'] = $selection;
				}
				elseif(count($diff = $this->diffStructurePermissive($value, $sqlData[$type][$key])))
				{
					$this->{$results}[$tbl][$key]['_status'] = 'mismatch';
					$this->{$results}[$tbl][$key]['_diff'] = $diff;
					$this->{$results}[$tbl][$key]['_valid'] = $value;
					$this->{$results}[$tbl][$key]['_invalid'] = $sqlData[$type][$key];
					$this->{$results}[$tbl][$key]['_file'] = $selection;
				}
			}
			if ($fileData['engine'] != $sqlData['engine'])
			{
				$this->errors[$tbl]['_status'] |= self::STATUS_TABLE_MISMATCH_STORAGE_ENGINE;
				$this->errors[$tbl]['_valid_' . self::STATUS_TABLE_MISMATCH_STORAGE_ENGINE] = $fileData['engine'];
				$this->errors[$tbl]['_invalid_' . self::STATUS_TABLE_MISMATCH_STORAGE_ENGINE] = $sqlData['engine'];
			}
			if ($fileData['charset'] != $sqlData['charset'])
			{
				$this->errors[$tbl]['_status'] |= self::STATUS_TABLE_MISMATCH_DEFAULT_CHARSET;
				$this->errors[$tbl]['_valid_' . self::STATUS_TABLE_MISMATCH_DEFAULT_CHARSET] = $fileData['charset'];
				$this->errors[$tbl]['_invalid_' . self::STATUS_TABLE_MISMATCH_DEFAULT_CHARSET] = $sqlData['charset'];
			}
		}
		return null;
	}
	
	/**
	 * Compile Results into a complete list of Fixes that could be run without the need of a form selection. 
	 */
	function compileResults()
	{
		foreach($this->results as $tabs => $field)
		{
			$file = varset($this->results[$tabs]['_file']);
			$errorStatus = is_int($this->errors[$tabs]['_status']) ?
				$this->errors[$tabs]['_status'] : self::STATUS_TABLE_OK;
			if($errorStatus & self::STATUS_TABLE_MISSING) // Missing Table
			{				
				$this->fixList[$file][$tabs]['all'][] = 'create';
			}
			elseif (
				$errorStatus & self::STATUS_TABLE_MISMATCH_STORAGE_ENGINE ||
				$errorStatus & self::STATUS_TABLE_MISMATCH_DEFAULT_CHARSET
			)
			{
				$this->fixList[$file][$tabs]['all'][] = 'convert';
			}
			foreach($field as $k=>$f)
			{
				if($f['_status']=='ok') continue;
				$status = $f['_status'];
				if(!empty($this->modes[$status]))
				{
					$this->fixList[$f['_file']][$tabs][$k][] = $this->modes[$status];
				}
			}
		}
		
		// Index
		if(count($this->indices))
		{
			foreach($this->indices as $tabs => $field)
			{
				if($this->errors[$tabs] != 'ok')
				{
					foreach($field as $k=>$f)
					{
						if($f['_status']=='ok') continue;
						$this->fixList[$f['_file']][$tabs][$k][] = $this->modes[$f['_status']];
					}	
				}				
			}		
		}
	}
	/** 
	 * Returns the number of errors
	 */
	public function errors()
	{
		$badTableCount = 0;
		foreach ($this->errors as $tableName => $tableMetadata)
		{
			if (!empty($tableMetadata['_status']))
			{
				$badTableCount++;
				continue;
			}
			foreach ($this->results[$tableName] as $fieldMetadata)
			{
				if (isset($fieldMetadata['_status']) && $fieldMetadata['_status'] != 'ok')
				{
					$badTableCount++;
					continue 2;
				}
			}
			foreach ($this->indices[$tableName] as $indexMetadata)
			{
				if (isset($indexMetadata['_status']) && $indexMetadata['_status'] != 'ok')
				{
					$badTableCount++;
					continue 2;
				}
			}
		}
		return $badTableCount;
	}
	/**
	 * @param $fileArray
	 * @return void
	 */
	function renderResults($fileArray=array())
	{
		
		$frm = e107::getForm();
		$ns = e107::getRender();
		$mes = e107::getMessage();
		
		$text = "
		
		";
	
		
		$ns->tablerender(DBVLAN_23.SEP.DBVLAN_16, $mes->render().$text);
		
	}
	/**
	 * @param $tabs
	 * @return mixed|string
	 */
	function renderTableName($tabs)
	{
		
		if(strpos($tabs,"lan_") === 0)
		{
			list($tmp,$lang,$table) = explode("_",$tabs,3);
			return $table. " (".ucfirst($lang).")";
		}
		return $tabs;
	}
	/**
	 * @param $file
	 * @param $table
	 * @param $field
	 * @param $newvalue
	 * @param $mode
	 * @param $after
	 * @return string
	 */
	function fixForm($file, $table, $field, $newvalue, $mode, $after ='')
	{
		$frm = e107::getForm();
		$text = $frm->checkbox("fix[$file][$table][$field][]", $mode, false, array('id'=>false));
		
		return $text;
	}
	/**
	 * @param $data
	 * @param $mode
	 * @return string
	 */
	function renderNotes($data, $mode='field')
	{
		// return "".print_r($data,TRUE)."
";
		
		$v = $data['_valid'];
		$i = $data['_invalid'];
		
		$valid = $this->toMysql($v,$mode);
        $invalid = $this->toMysql($i,$mode);
        
		$text = "";
		if($invalid)
		{
			$text .= "".DBVLAN_9."
				".$invalid."
";
		}
		
		$text .= "".DBVLAN_10."
			".$valid."
";
			
		return $text;
	}
	/**
	 * @param $data
	 * @param $mode
	 * @return string|void
	 */
	public function toMysql($data, $mode = 'field')
	{
		
		if(!$data) return;
		
		if($mode === 'index')
		{
			// print_a($data);
			if($data['type'])
			{
				//return $data['type']." (".$data['field'].");";
				// Check if index keyname exists and add backticks
				$keyname = (!empty($data['keyname']) ? " `".$data['keyname']."`" : "");
				return $data['type'] . $keyname . " (" . $data['field'] . ");";
			}
			else
			{
				return "INDEX `".$data['keyname']."` (".$data['field'].");";
			}
			
		}
		
		if(!in_array(strtolower($data['type']), $this->fieldTypes))
		{
			$ret = $data['type']."(".$data['value'].") ".$data['attributes']." ".$data['null']." ".$data['default'];
			return trim($ret);
		}
		else
		{
			$ret = $data['type'];
			$ret .= !empty($data['attributes']) ? " ".$data['attributes'] : '';
			$ret .= !empty($data['null']) ? " ".$data['null'] : '';
			$ret .= !empty($data['default']) ? " ".$data['default'] : '';
			return $ret;
		}
           
	}
	
	// returns the previous Field
	/**
	 * @param $array
	 * @param $cur
	 * @return int|mixed|string
	 */
	function getPrevious($array, $cur)
	{
		$fkeys = array_keys($array);
		$cur_key = array_search($cur, $fkeys);
		return @$fkeys[$cur_key - 1];
	}
	
	/**
	* get the key ID for the current table which is being Fixed. 
	*/ 
	function getId($tabl,$cur)
	{
		if(empty($tabl))
		{
			return null;
		}
		$key = array_flip($tabl);
		
		if(strpos($cur,"lan_") === 0) // language table adjustment.
		{
			list($tmp,$lang,$cur) = explode("_",$cur,3);
		}
		
		if(isset($key[$cur]))
		{
			return $key[$cur];
		} 
	}
	/**
	 * @param string $mode index|alter|insert|drop|create|indexdrop
	 * @param string $table eg. submitnews
	 * @param string $field eg. submitnews_id
	 * @param string $sqlFileData (after CREATE)  eg. dblog_id int(10) unsigned NOT NULL auto_increment, ..... KEY....
	 * @param string $engine MyISAM|InnoDB
	 * @param string $charset MySQL/MariaDB text character set
	 * @return string SQL query
	 */
	function getFixQuery(
		$mode,
		$table,
		$field,
		$sqlFileData,
		$engine = self::MOST_PREFERRED_STORAGE_ENGINE,
		$charset = self::MOST_PREFERRED_CHARSET
	)
	{
		if(strpos($mode, 'index') === 0)
		{
			$fdata = $this->getIndex($sqlFileData);
			$newval = $this->toMysql($fdata[$field],'index');
		}
		else
		{
			$fdata = $this->getFields($sqlFileData);
			$newval = $this->toMysql($fdata[$field]);
		}
		$query = "";
		switch($mode)
		{
			case 'alter':
				$query = "ALTER TABLE `".MPREFIX.$table."` CHANGE `$field` `$field` $newval";
			break;
			case 'insert':
				$after = ($aft = $this->getPrevious($fdata,$field)) ? " AFTER {$aft}" : "";
				$query = "ALTER TABLE `".MPREFIX.$table."` ADD `$field` $newval{$after}";
			break;
			case 'drop':
				$query = "ALTER TABLE `".MPREFIX.$table."` DROP `$field`";
			break;
			case 'index':
				$newval = str_replace("PRIMARY", "PRIMARY KEY", $newval);
				$query = "ALTER TABLE `".MPREFIX.$table."` ADD ".$newval;
			break;
			case 'indexdrop':
				$query = "ALTER TABLE `".MPREFIX.$table."` DROP INDEX `$field`";
			break;
			case 'create':
				$query = "CREATE TABLE `".MPREFIX.$table."` (".$sqlFileData.")".
					" ENGINE=".$engine." DEFAULT CHARACTER SET=".$charset.";";
			break;
			case 'convert':
				$showCreateTable = $this->getSqlData($table);
				$currentSchema = $this->getSqlFileTables($showCreateTable);
				if ($engine != $currentSchema['engine'][0])
					$query .= "ALTER TABLE `".MPREFIX.$table."` ENGINE=".$engine.";";
				if ($charset != $currentSchema['charset'][0])
					$query .= "ALTER TABLE `".MPREFIX.$table."` CONVERT TO CHARACTER SET ".$charset.";";
				break;
		}
		return $query;
	}
	/**
	 * Fix tables
	 * FixArray eg. [core][table][field] = alter|create|index| etc. 
	 */
	function runFix($fixArray='')
	{
		$mes  = e107::getMessage();
		$log = e107::getLog();
		
		if(!is_array($fixArray))
		{
			$fixArray = $this->fixList;	// Fix All	
		}
				
			
		foreach($fixArray as $j=>$file)
		{
						
			foreach($file as $table=>$val)
			{
				
				$id = $this->getId($this->sqlFileTables[$j]['tables'],$table);
				$toFix = count($val);
				foreach($val as $field=>$fixes)
				{
					foreach($fixes as $mode)
					{				
						$query = $this->getFixQuery(
							$mode,
							$table,
							$field,
							$this->sqlFileTables[$j]['data'][$id],
							$this->getIntendedStorageEngine($this->sqlFileTables[$j]['engine'][$id]),
							$this->getIntendedCharset($this->sqlFileTables[$j]['charset'][$id])
						);
						
				
						// $mes->addDebug("Query: ".$query);		
						// continue;	
						 
						 
						if(e107::getDb()->gen($query) !== false)
						{
							$log->addDebug(LAN_UPDATED.'  ['.$query.']');
							$toFix--;
						} 
						else 
						{
							$log->addWarning(LAN_UPDATED_FAILED.'  ['.$query.']');
							$log->addWarning(e107::getDb()->getLastErrorText()); // PDO compatible.
							/*if(mysql_errno())
							{
								$log->addWarning('SQL #'.mysql_errno().': '.mysql_error());
							}*/
						}
					}	
				}
				if(empty($toFix))
				{
					unset($this->errors[$table], $this->fixList[$j][$table]); // remove from error list since we are using a singleton
				}
			}	//
		}
		$log->flushMessages("Database Table(s) Modified");
				
	}
	/**
	 * @param $sql_data
	 * @return array|false
	 */
	function getSqlFileTables($sql_data)
	{
		if(!$sql_data)
		{
			e107::getMessage()->addError("No SQL Data found in file");
			return false;
		}
		
		$ret = array();
		$sql_data = preg_replace("#\/\*.*?\*\/#mis", '', $sql_data);	// remove comments 
	 	$regex = "/CREATE TABLE (?:IF NOT EXISTS )?`?([\w]*)`?\s*?\(([^;]*)\)\s*((?:[\w\s]+=[^\s]+)+\s*)*;/i";
		preg_match_all($regex,$sql_data,$match);
		$tables = array();
			
		foreach($match[1] as $c=>$k)
		{
			if(strpos($k,'e107_') === 0) // remove prefix if found in sql dump.
			{
				$k = substr($k, 5);	
			}
			
			$tables[$c] = $k;		
		}		
				
				
		$ret['tables'] = $tables;
		$data = array();
		if(!empty($match[2])) // clean/trim data.
		{
			foreach($match[2] as $dat)
			{
				$dat = str_replace("\t", '', $dat); // remove tab chars.
				$data[] = trim($dat);
			}
		}
		$ret['data'] = $data;
		$ret['engine'] = array();
		$ret['charset'] = array();
		foreach ($match[3] as $rawTableOptions)
		{
			if (empty($rawTableOptions)) continue;
			$engine = null;
			$charset = null;
			$tableOptionsRegex = "/([\w\s]+=[\w]+)+?\s*/";
			preg_match_all($tableOptionsRegex, $rawTableOptions, $tableOptionsSplit);
			$tableOptionsSplit = current($tableOptionsSplit);
			foreach ($tableOptionsSplit as $rawTableOption)
			{
				list($tableOptionName, $tableOptionValue) = explode("=", $rawTableOption, 2);
				$tableOptionName = strtoupper(trim($tableOptionName));
				$tableOptionValue = trim($tableOptionValue);
				switch ($tableOptionName)
				{
					case "ENGINE":
					case "TYPE":
						$engine = $tableOptionValue;
						break;
					case "DEFAULT CHARSET":
					case "DEFAULT CHARACTER SET":
					case "CHARSET":
					case "CHARACTER SET":
						$charset = $tableOptionValue;
						break;
				}
			}
			$ret['engine'][] = $engine;
			$ret['charset'][] = $charset;
		}
		if(empty($ret['tables']))
		{
			e107::getMessage()->addDebug("Unable to parse ".$this->currentTable."_sql.php file data. Possibly missing a ';' at the end?");
			e107::getMessage()->addDebug(print_a($regex,true));
		}
		
		return $ret;
	}
	/**
	 * @param $data
	 * @param $print
	 * @return array
	 */
	function getFields($data, $print = false)
	{
		
		// Clean $data and add ` ` arond field-names - prevents issues when field == field-type. 
		$tmp = explode("\n",$data);
		$newline = array();
		
		foreach($tmp as $line)
		{
			$line = trim($line);
			$newline[] = preg_replace('/^([^`\s][0-9a-zA-Z\$_]*)/',"`$1`", $line);
		}
		$data = implode("\n",$newline);
		// --------------------
		
		$mes = e107::getMessage();
			
	//	$regex = "/`?([\w]*)`?\s*?(".implode("|",$this->fieldTypes)."|".implode("|",$this->fieldTypeNum).")\s?(?:\([\s]?([0-9,]*)[\s]?\))?[\s]?(unsigned)?[\s]?.*?(?:(NOT NULL|NULL))?[\s]*(auto_increment|default .*)?[\s]?(?:PRIMARY KEY)?[\s]*?,?\s*?\n/im";
		$regex = "/^\s*?`?([\w]*)`?\s*?(".implode("|",$this->fieldTypes)."|".implode("|",$this->fieldTypeNum).")\s?(?:\([\s]?([0-9,]*)[\s]?\))?[\s]?(unsigned)?[\s]?.*?(?:(NOT NULL|NULL))?[\s]*(auto_increment|default|AUTO_INCREMENT|DEFAULT [\w'\s.\(:\)-]*)?[\s]?(comment [\w\s'.-]*)?[\s]?(?:PRIMARY KEY)?[\s]*?,?\s*?\n/im";
	//	echo $regex."
";
	
		//	$regex = "/`?([\w]*)`?\s*(int|varchar|tinyint|smallint|text|char|tinyint) ?(?:\([\s]?([0-9]*)[\s]?\))?[\s]?(unsigned)?[\s]?.*?(NOT NULL|NULL)?[\s]*(auto_increment|default .*)?[\s]?,/i";		
		
	//	$regex = "/^\s*?`?([\w]*)`?\s*?(".implode("|",$this->fieldTypes)."|".implode("|",$this->fieldTypeNum).")\s?(?:\([\s]?([0-9,]*)[\s]?\))?[\s]?(unsigned)?[\s]?.*?(?:(NOT NULL|NULL))?[\s]*?(auto_increment|default [\w'\".-]*)?[\s]?(?:PRIMARY KEY)?[\s]*?,?\n/im";
	//$regex = "/^\s*?`?([\w]*)`?\s*?(date|time|timestamp|datetime|year|tinyblob|blob|mediumblob|longblob|tinytext|mediumtext|longtext|text|bit|tinyint|smallint|mediumint|integer|int|bigint|real|double|float|decimal|numeric|varchar|char|binary|varbinary|enum|set)\s?(?:\([\s]?([0-9,]*)[\s]?\))?[\s]?(unsigned)?[\s]*?(?:(NOT NULL|NULL))?[\s]*?(auto_increment|default [\w'\".-]*)?[\s]?(?:PRIMARY KEY)?[\s]*?,?\n/im";
	//	$mes->addDebug($regex);
	
	//$regex = "/^\s*?`?([\w]*)`?\s*?(date|time|timestamp|datetime|year|text|bit|tinyint|smallint|mediumint|integer|int|bigint|real|double|float|decimal|numeric|varchar|char|binary|varbinary|enum|set)\s?(?:\([\s]?([0-9,]*)[\s]?\))?[\s]?(unsigned)?[\s]*?(?:(NOT NULL|NULL))?[\s]*?(auto_increment|default [\w'.-]*)?[\s]?(?:PRIMARY KEY)?[\s]*?,?\n/i";
		
	//	echo "reg=".$regex;
		
		preg_match_all($regex,$data,$m);	
		
		$ret = array();
		
	 if($print) var_dump($regex, $m);
			
		foreach($m[1] as $k=>$val)
		{
			$ret[$val] = array(
				'type'			=> trim(strtoupper($m[2][$k])),
				'value'			=> $m[3][$k],
				'attributes'	=> strtoupper($m[4][$k]),
				'null'			=> strtoupper($m[5][$k]),
				'default'		=> strtoupper($m[6][$k])
			);
		}
		
		return $ret;
	}
	/**
	 * @param $data
	 * @param $print
	 * @return array
	 */
	function getIndex($data, $print = false)
	{
		$regex = "/(PRIMARY|UNIQUE|FULLTEXT|FOREIGN)?[\s]*?(INDEX|KEY)[\s]*(?:`?([\w]*)`?)?[\s]*?\(`?([\w]+)`?(?:\s*\(\d+\))?(?:\s*(ASC|DESC))?\)[\s]*,?/i";
		preg_match_all($regex,$data,$m);
		if (count($m) > 0)
		{
			unset($m[2]);
			$m = array_combine(range(0, count($m)-1), array_values($m));
		}
		$ret = array();
		
		if($print)
		{
			e107::getDebug()->log($m);
		}
		
		// Standard Detection Method.
		$fieldReplace = array("`"," ");
		foreach($m[3] as $k=>$val)
		{
			if(!$val) continue;
			$val = str_replace("`","",$val);
			$key = !empty($m[2][$k]) ? $m[2][$k] : $val;
			$ret[$key] = array(
				'type'		=> strtoupper($m[1][$k]),
				'keyname'	=> (!empty($m[2][$k])) ? str_replace("`","",$m[2][$k]) : str_replace("`","",$m[3][$k]),
				'field'		=> str_replace($fieldReplace,"",$m[3][$k])
			);
		}
		
		//Alternate Index detection method. 
		// eg.  `table_id` INT( 10 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
		
		$regex = "/`?([\w]*)`? .*((?:AUTO_INCREMENT))\s?(PRIMARY|UNIQUE|FULLTEXT|FOREIGN)\s?KEY\s?,/i";
		preg_match_all($regex,$data,$m);
		
		foreach($m[1] as $k=>$val)
		{
			if(!$val) continue;
			
		    $ret[$val] = array(
				'type'		=> strtoupper($m[3][$k]),
				'keyname'	=> $m[1][$k],
				'field'		=> str_replace($fieldReplace,"",$m[1][$k])
			);	
		}
		if($print)
		{
			e107::getDebug()->log($ret);
		}
		return $ret;
	}
	/**
	 * @param $tbl
	 * @param $language
	 * @return false|string
	 */
	function getSqlData($tbl, $language='')
	{
		
		$mes = e107::getMessage();
		$prefix = MPREFIX;
		
		if($language)
		{
			if(!in_array($tbl,$this->sqlLanguageTables[$language]))
			{
				return FALSE;
			}
			
			$prefix .= "lan_".$language."_";
			// $mes->addDebug("Retrieving Language Table Data: ".$prefix . $tbl."
"); 				
		}
		$sql = e107::getDb();
		if(!$sql->isTable($tbl))
		{
			$mes->addDebug('Missing table on db-verify: '.$tbl);
			return false;
		}
	//	mysql_query('SET SQL_QUOTE_SHOW_CREATE = 1');
		$qry = 'SHOW CREATE TABLE `' . $prefix . $tbl . "`";
		
		
	//	$z = mysql_query($qry);
		$z = $sql->gen($qry);
		if($z)
		{
		//	$row = mysql_fetch_row($z);
			$row = $sql->fetch('num');
			//return $row[1];
			return stripslashes($row[1]).';'; // backticks needed. 
			// return str_replace("`", "", stripslashes($row[1])).';';
		}
		else
		{
			$mes->addDebug('Failed: '.$qry);
			$this->internalError = true;
			return FALSE;
		}
	
	}
	/**
	 * @return array
	 */
	function getSqlLanguages()
	{
		$sql = e107::getDb();
		$list = $sql->tables('lan');
		
		$array = array();
		
		foreach($list as $tb)
		{
			list($tmp,$lang,$table) = explode("_",$tb,3);
			$array[$lang][] = $table;
		}
		
		return $array;
	}
	/**
	 * @return void
	 */
	function renderTableSelect()
	{
		$frm = e107::getForm();
		$ns = e107::getRender();
		$mes = e107::getMessage();
		
		
		$text = "
		
		";
	
		$ns->tablerender(DBVLAN_23.SEP.DBVLAN_16, $mes->render().$text);
	}
	/**
	 * Get the available storage engines on this MySQL server
	 *
	 * This method is not memoized and should not be called repeatedly.
	 *
	 * @return string[] An unordered list of the storage engines supported by the current MySQL server
	 */
	private static function getAvailableStorageEngines()
	{
		$db = e107::getDb();
		$db->gen("SHOW ENGINES;");
		$output = [];
		while ($row = $db->fetch())
		{
			$output[] = $row['Engine'];
		}
		return $output;
	}
	/**
	 * Get the most compatible MySQL storage engine on this server for the provided storage engine
	 *
	 * @param string|null $maybeStorageEngine The requested storage engine
	 * @return string|false The MySQL storage engine that should actually be used. false if no match found.
	 */
	public function getIntendedStorageEngine($maybeStorageEngine = null)
	{
		if ($maybeStorageEngine === null)
			return $this->getIntendedStorageEngine(self::MOST_PREFERRED_STORAGE_ENGINE);
		if (!array_key_exists($maybeStorageEngine, $this->storageEnginePreferenceMap))
		{
			if (in_array($maybeStorageEngine, $this->availableStorageEngines))
				return $maybeStorageEngine;
			return false;
		}
		$fit = array_intersect($this->storageEnginePreferenceMap[$maybeStorageEngine], $this->availableStorageEngines);
		return current($fit);
	}
	/**
	 * Try to figure out what storage engine the provided one is referring to
	 *
	 * @param string $maybeStorageEngine The reported storage engine
	 * @return string The probable storage engine the input is referring to
	 * @throws UnexpectedValueException if the provided storage engine is not known as an available storage engine
	 */
	public function getCanonicalStorageEngine($maybeStorageEngine)
	{
		if (in_array($maybeStorageEngine, $this->availableStorageEngines))
			return $maybeStorageEngine;
		throw new UnexpectedValueException(
			"Unknown storage engine: " . var_export($maybeStorageEngine, true)
		);
	}
	/**
	 * Get the most compatible MySQL character set based on the input
	 *
	 * @param string|null $maybeCharset The requested character set. null to retrieve the default
	 * @return string The MySQL character set that should actually be used
	 */
	public function getIntendedCharset($maybeCharset = null)
	{
		if (empty($maybeCharset)) return self::MOST_PREFERRED_CHARSET;
		return $this->getCanonicalCharset($maybeCharset);
	}
	/**
	 * Try to figure out what character set the provided one is referring to
	 *
	 * @param string $maybeCharset The reported character set
	 * @return string The probable character set
	 */
	public function getCanonicalCharset($maybeCharset)
	{
		if ($maybeCharset == "utf8") return "utf8mb4";
		return $maybeCharset;
	}
	
	
}
/*
function check_tables($what)
{
	global $tablines, $table_list, $frm, $emessage;
	$cur = 0;
	$table_list = "";
	read_tables($what);
	$fix_active = FALSE;			// Flag set as soon as there's a fix - enables 'Fix it' button
	$text = "
		
	";
	return $text;
}
global $table_list;
// -------------------- Table Fixing ------------------------------
if(isset($_POST['do_fix']))
{
	//$emessage->add(DBVLAN_20);
	foreach( $_POST['fix_active'] as $key=>$val)
	{
		if (MAGIC_QUOTES_GPC == TRUE)
		{
			$table = stripslashes($_POST['fix_table'][$key][0]);
			$newval = stripslashes($_POST['fix_newval'][$key][0]);
			$mode = stripslashes($_POST['fix_mode'][$key][0]);
			$after = stripslashes($_POST['fix_after'][$key][0]);
		}
		else
		{
			$table = $_POST['fix_table'][$key][0];
			$newval = $_POST['fix_newval'][$key][0];
			$mode = $_POST['fix_mode'][$key][0];
			$after = $_POST['fix_after'][$key][0];
		}
		$field= $key;
		
		switch($mode)
		{
			case 'alter':
				$query = "ALTER TABLE `".MPREFIX.$table."` CHANGE `$field` `$field` $newval";
			break;
			case 'insert':
				if($after) $after = " AFTER {$after}";
				$query = "ALTER TABLE `".MPREFIX.$table."` ADD `$field` $newval{$after}";
			break;
			
			case 'drop':
				$query = "ALTER TABLE `".MPREFIX.$table."` DROP `$field` ";
			break;
			
			case 'index':
				$query = "ALTER TABLE `".MPREFIX.$table."` ADD INDEX `$field` ($newval)";
			break;
			
			case 'indexalt':
				$query = "ALTER TABLE `".MPREFIX.$table."` ADD $field ($newval)";
			break;
			
			case 'indexdrop':
				$query = "ALTER TABLE `".MPREFIX.$table."` DROP INDEX `$field`";
			break;
			
			case 'create':
			$query = "CREATE TABLE `".MPREFIX.$table."` ({$newval}";
			if (!preg_match('#.*?\s+?(?:TYPE|ENGINE)\s*\=\s*(.*?);#is', $newval))
			{
				$query .= ') TYPE=MyISAM;';
			}
			break;
		}
		return $query;
		//FIXME - db handler!!!
		if(mysql_query($query)) $emessage->add(LAN_UPDATED.' [ '.$query.' ]', E_MESSAGE_SUCCESS);
		else 
		{
			$emessage->add(LAN_UPDATED_FAILED.' [ '.$query.' ]', E_MESSAGE_WARNING);
			if(mysql_errno())
			{
				$emessage->add('     SQL #'.mysql_errno().': '.mysql_error(), E_MESSAGE_WARNING);
			}
		}
	}
}
$text = "
	
";
$e107->ns->tablerender(DBVLAN_23.' - '.DBVLAN_16, $emessage->render().$text);
require_once(e_ADMIN."footer.php");
exit;
// --------------------------------------------------------------
function fix_form($table,$field, $newvalue,$mode,$after ='')
{
	global $frm;
	
	if($mode == 'create')
	{
		$newvalue = implode("\n",$newvalue);
		$field = $table;		// Value for $field may be rubbish!
	}
	else
	{
		if(stristr($field, 'KEY ') !== FALSE)
		{
			$field = chop(str_replace('KEY ','',$field));
			$mode = ($mode == 'drop') ? 'indexdrop' : 'index';
			$search = array('(', ')');
			$newvalue = str_replace($search,'',$newvalue);
			$after = '';
		}
		
		if($mode == 'index' && (stristr($field, 'FULLTEXT ') !== FALSE || stristr($field, 'UNIQUE ') !== FALSE))
		{
			$mode = 'indexalt';
		}
		elseif($mode == 'indexdrop' && (stristr($field, 'FULLTEXT ') !== FALSE || stristr($field, 'UNIQUE ') !== FALSE))
		{
			$field = trim(str_replace(array('FULLTEXT ', 'UNIQUE '), '', $field));
		}
		$field = trim($field, '`');
	}
	$text .= "\n\n";
	$text .= $frm->checkbox("fix_active[$field][]", 1, false, array('id'=>false));
	$text .= "\n";
	$text .= "\n";
	$text .= "\n";
	$text .= ($after) ? "\n" : "";
	$text .= "\n\n";
	return $text;
}
function table_list()
{
	// grab default language lists.
	global $mySQLdefaultdb;
	$exclude[] = "banlist";		$exclude[] = "banner";
	$exclude[] = "cache";		$exclude[] = "core";
	$exclude[] = "online";		$exclude[] = "parser";
	$exclude[] = "plugin";		$exclude[] = "user";
	$exclude[] = "upload";		$exclude[] = "userclass_classes";
	$exclude[] = "rbinary";		$exclude[] = "session";
	$exclude[] = "tmp";	 		$exclude[] = "flood";
	$exclude[] = "stat_info";	$exclude[] = "stat_last";
	$exclude[] = "submit_news";	$exclude[] = "rate";
	$exclude[] = "stat_counter";$exclude[] = "user_extended";
	$exclude[] = "user_extended_struct";
	$exclude[] = "pm_messages";
	$exclude[] = "pm_blocks";
	
	$replace = array();
	
	$lanlist = explode(",",e_LANLIST);
	foreach($lanlist as $lang)
	{
		if($lang != $pref['sitelanguage'])
		{
			$replace[] = "lan_".strtolower($lang)."_";
		}
	}
	$tables = mysql_list_tables($mySQLdefaultdb);
	while (list($temp) = mysql_fetch_array($tables))
	{
		$prefix = MPREFIX."lan_";
		$match = array();
		if(strpos($temp,$prefix)!==FALSE)
		{
			$e107tab = str_replace(MPREFIX, "", $temp);	
			$core = str_replace($replace,"",$e107tab);
			if (str_replace($exclude, "", $e107tab))
			{
				$tabs[$core] = $e107tab;
			}		
		}
	}
	return $tabs;
}
*/