<?php
/**
 * @brief		Conversion module
 * @author		<a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) 2001 - SVN_YYYY Invision Power Services, Inc.
 * @license		http://www.invisionpower.com/legal/standards/
 * @package		IPS Tools
 * @since		4 Sept 2013
 * @version		SVN_VERSION_NUMBER
 */

namespace IPSUtf8;

/**
 * Conversion class
 */
class Convert
{
	const VERSION_ID = '1.1.17';

	/**
	 * @brief	Data Store
	 */
	protected $_data = array();

	/**
	 * @brief	Instance
	 */
	protected static $instance = NULL;

	/**
	 * @brief	Convertable table fields
	 */
	protected static $convertCols = array( 'text', 'mediumtext', 'longtext', 'varchar', 'char', 'tinytext' );

	/**
	 * @brief	Numeric table fields
	 */
	protected static $numericCols = array( 'integer' => 11, 'int' => 11, 'smallint' => 6, 'tinyint' => 4, 'mediumint' => 8, 'bigint' => 20, 'decimal' => null, 'numeric' => null, 'float' => null, 'double' => null );

	/**
	 * @brief	Tables we need to convert
	 */
	protected static $nonUtf8Tables = NULL;

	/**
	 * @brief	Tables we need to convert
	 */
	protected static $nonUtf8Collations = NULL;

	/**
	 * @brief	Number of rows we need to convert
	 */
	protected static $rowsToConvert = NULL;

	/**
	 * @brief	Native Database object
	 */
	protected static $db = NULL;

	/**
	 * @brief	UTF-8 Database object
	 */
	protected static $utf = NULL;

	/**
	 * @brief	UTF-8 table engine
	 */
	protected static $utfTableEngine = NULL;

	/**
	 * @brief	Create don't populate
	 */
	protected static $createOnly = array( 'content_cache_posts', 'content_cache_sigs', 'sessions', 'topic_views', 'search_keywords' );
	
	/**
	 * @brief	Problem Tables that can cause an error due to serialization while using "fast" mode
	 */
	protected static $problemTables = array( 'cache_store' );

	/**
	 * @brief	MySQL supported character sets
	 */
	protected static $mysqlCharSets = NULL;

	/**
	 * Get instance
	 *
	 * @return	Output
	 */
	public static function i()
	{
		if ( self::$instance === NULL )
		{
			static::$db  = \IPSUtf8\Db::i();
			if ( defined( 'SOURCE_DB_CHARSET' ) AND SOURCE_DB_CHARSET !== NULL )
			{
				static::$db->set_charset( SOURCE_DB_CHARSET );
			}
			static::$utf = \IPSUtf8\Db::i('utf8');
			self::$instance = new self();
			self::$instance->init();
		}

		return self::$instance;
	}

	/**
	 * Get value from data store
	 *
	 * @param	mixed	$key	Key
	 * @return	mixed	Value from the datastore
	 */
	public function __get( $key )
	{
		if( isset( $this->_data[ $key ] ) )
		{
			return $this->_data[ $key ];
		}

		return NULL;
	}

	/**
	 * Set value in data store
	 *
	 * @param	mixed	$key	Key
	 * @param	mixed	$value	Value
	 * @return	void
	 */
	public function __set( $key, $value )
	{
		if( array_key_exists( $key, $this->_data ) )
		{
			$this->_data[ $key ] = $value;
		}
	}

	/**
	 * Process a batch
	 *
	 * @param	int|null	Number of rows to limit per batch
	 * @return	boolean		True (I converted some) False (I didn't)
	 */
	public function process( $limit=null, $fromError=false )
	{
		/* MB4? */
		$sessionData = \IPSUtf8\Session::i()->json;

		if ( isset( $sessionData['use_utf8mb4'] ) AND ! empty( $sessionData['use_utf8mb4'] ) )
		{
			static::$utf->set_charset( 'utf8mb4' );
		}

		if ( \IPSUtf8\Session::i()->current_table === NULL AND \IPSUtf8\Session::i()->is_ipb )
		{
			/* Lock tasks until the end of time so they do not attempt to add / remove things during conversion */
			if( static::$db->checkForTable('task_manager') )
			{
				static::$db->update( "task_manager", array( 'task_locked' => 2147483647 ) );
			}
			else
			{
				static::$db->update( "core_tasks", array( 'next_run' => 2147483647 ) );
			}
		}

		$convertCount   = 0;
		$table          = $this->getTable();
		$convertCols    = static::getConvertableColumns( $table['name'] );
		$numericCols    = static::getNumericColumns( $table['name'] );
		$tableCharSet   = \IPSUtf8\Convert::i()->database_charset;
		$currentCharSet = \IPSUtf8\Session::i()->current_charset;

		if ( ! empty( $table['charset'] ) )
		{
			$tableCharSet = $table['charset'];

			/* Latin1 is actually cp1252, not ISO-8859-1 @link http://dev.mysql.com/doc/refman/5.0/en/charset-we-sets.html */
			/* But only if our document character set is ISO-8859-1 or UTF-8. Otherwise, we're using the slow method which needs to real charset despite what the table is set too */
			if ( $tableCharSet === 'latin1' AND in_array( mb_strtolower( $currentCharSet ), array( 'iso-8859-1', 'utf-8' ) ) )
			{
				$currentCharSet = 'windows-1252';
			}
			
			if ( defined( 'FORCE_CONVERT_CHARSET' ) AND FORCE_CONVERT_CHARSET !== NULL )
			{
				$currentCharSet = FORCE_CONVERT_CHARSET;
			}
		}

		if ( !defined( 'SOURCE_DB_CHARSET' ) OR SOURCE_DB_CHARSET === NULL )
		{
			static::$db->set_charset( $tableCharSet );
		}

		/* Create table only */
		if ( IPB_LOCK and in_array( $table['name'], static::$createOnly ) )
		{
			static::log( 'No content conversion of ' . $table['name'] . ' required' );

			$this->getNextTable( $table['name'] );

			\IPSUtf8\Session::i()->status = 'processing';
			\IPSUtf8\Session::i()->json   = $sessionData;
			\IPSUtf8\Session::i()->save();

			return true;
		}

		while( $convertCount < $limit )
		{
			$sessionData  = \IPSUtf8\Session::i()->json;

			if ( $table === null )
			{
				if ( count( array_keys( \IPSUtf8\Session::i()->tables ) ) == count( array_keys( \IPSUtf8\Session::i()->completed_json ) ) )
				{
					\IPSUtf8\Session::i()->status = 'completed';
					\IPSUtf8\Session::i()->save();
					return false;
				}

				return false;
			}

			/* Got any fields to convert? */
			if ( count( $convertCols ) === 0 )
			{
				/* No data to convert, lets repopulate the easy way! */
				$this->preInsert( $table );

				static::$utf->delete( $table['name'] );
				static::$utf->insert( $table['name'], static::$db->select( '*', $table['name'] ) );

				$this->postInsert( $table );

				/* Update counts */
				$sessionData['convertedCount'] += $table['count'];
				$convertCount			       += $table['count'];

				static::log( 'No columns to convert in ' . $table['name'] . ' INSERT INTO FROM SELECT used' );

				$table        = $this->getNextTable( $table['name'] );
				$convertCols  = static::getConvertableColumns( $table['name'] );
				$numericCols  = static::getNumericColumns( $table['name'] );

				if ( $table === null or $convertCount >= $limit )
				{
					\IPSUtf8\Session::i()->status = 'processing';
					\IPSUtf8\Session::i()->json   = $sessionData;
					\IPSUtf8\Session::i()->save();

					return true;
				}
			}
			
			/* If the previous table had no columns to convert, then the table charset may be lost - reset it if it does not match */
			if ( $table !== NULL AND $tableCharSet !== $table['charset'] )
			{
				$tableCharSet = $table['charset'];
			}
			
			/* Are we reeeeeeeeally sure we know the charset? Let's check one more time */
			if ( ! $tableCharSet )
			{
				$createTable = static::$db->query( "SHOW CREATE TABLE `" . static::$db->prefix . $table['name'] . "`" )->fetch_assoc();
				
				if ( preg_match( '#\scharset=([a-z0-9]+?)(\s|$)#i', $createTable['Create Table'], $matches ) )
				{
					$tableCharSet = mb_strtolower( $matches[1] );
				}
			}

			/* Latin1 or ISO-8559-1? */
			if ( ! in_array( $table['name'], static::$problemTables ) AND ( in_array( $tableCharSet, $this->getMysqlCharSets() ) ) and ( \IPSUtf8\Session::i()->current_charset == 'utf-8' or \IPSUtf8\Session::i()->current_charset == 'iso-8859-1' ) AND ( !isset( $sessionData['force_conversion'] ) OR $sessionData['force_conversion'] !== 1 ) )
			{
				/* Convert the fast way! */
				$count = static::$utf->select( 'COUNT(*)', $table['name'] )->first();
				if ( $count > 0 )
				{
					/* TRUNCATE can hang so only do it if the table isn't empty (which it will be 99% of the time) */
					/* @see http://dba.stackexchange.com/questions/28055/truncate-table-statement-sometimes-hangs */
					static::log("TRUNCATE TABLE `" . static::$utf->prefix . $table['name'] . "`");

					static::$utf->query("TRUNCATE TABLE `" . static::$utf->prefix . $table['name'] . "`" );
				}

				/* Update counts */
				$sessionData['convertedCount'] += $table['count'];
				$convertCount			       += $table['count'];

				$select      	= array();
				$convertCols 	= static::getConvertableColumns( $table['name'] );
				$schematic		= static::getTableSchematic( $table['name'] );

				foreach( array_keys( $table['definition']['columns'] ) as $col )
				{
					if ( in_array( $col, $convertCols ) )
					{
						/* Even if the current charset is set to UTF-8, if the Table Charset is latin1, we need to use this method */
						/* ... but only if the actual column isn't using utf8 */
						$columnCollation = $schematic['definition']['columns'][$col]['collation'];
						if ( \IPSUtf8\Session::i()->current_charset != 'utf-8' OR ( $tableCharSet != 'utf8' AND ! in_array( $columnCollation, array( 'utf8_unicode_ci', 'utf8mb4_unicode_ci' ) ) ) OR ( $tableCharSet == 'utf8' AND mb_substr( $columnCollation, 0, 6 ) == 'latin1' ) )
						{
							if ( \IPSUtf8\Session::i()->current_charset == 'iso-8859-1' AND $tableCharSet == 'latin1' )
							{
								/* Casting as binary truncates non-latin characters, so if we are using real Latin1, just convert */
								$select[] = 'CONVERT( `' . $col . '` USING utf8 )';
							}
							else
							{
								/* Otherwise cast to binary */
								$select[] = 'CONVERT( CAST(`' . $col . '` AS BINARY) USING utf8 )';
							}
						}
						else
						{
							/**
							 * @note This looks a little weird but bare with me. Semi-old databases (post-2.x but pre-4.x) can have UTF8 Tables and Columns, but contain Latin1 data.
							 * We need to try and sniff this out and apply a different type of conversion, because in this instance you cannot just extract
							 * as BINARY and then insert into the source. Instead, you need to convert to latin1, then to binary, then to utf8. But you can't
							 * do that on actual UTF8 data because it will then truncate multibyte characters so we need to compare the conversion against what
							 * is stored.
							 */
							$collation = 'utf8_unicode_ci';
							
							if ( $tableCharSet == 'utf8mb4' OR $columnCollation == 'utf8mb4_unicode_ci' )
							{
								$collation = 'utf8mb4_unicode_ci';
							}
							
							$source_charset = 'latin1';
							if ( defined( 'SOURCE_DB_CHARSET' ) AND SOURCE_DB_CHARSET !== NULL )
							{
								$source_charset = SOURCE_DB_CHARSET;
							}
							
							$select[] = 'CASE WHEN STRCMP(CONVERT(CONVERT(CONVERT(`' . $col . '` USING ' . $source_charset . ') USING BINARY) USING utf8), `' . $col . '` COLLATE ' . $collation . ') = 0 THEN CONVERT(`' . $col . '` USING BINARY) ELSE CONVERT(CONVERT(CONVERT(`' . $col . '` USING ' . $source_charset . ') USING BINARY) USING utf8) END';
						}
					}
					else
					{
						$select[] = '`' . $col . '`';
					}
				}

				$this->preInsert( $table );

				$sql = "INSERT IGNORE INTO `" . static::$utf->prefix . $table['name'] . "` SELECT " . implode( ',', $select ) . " FROM `" . static::$db->prefix . $table['name'] . "`";

				static::log( $sql );

				static::$utf->query( $sql );

				$this->postInsert( $table );

				$table = $this->getNextTable( $table['name'] );

				\IPSUtf8\Session::i()->status = 'processing';
				\IPSUtf8\Session::i()->json   = $sessionData;
				\IPSUtf8\Session::i()->save();

				/* Always reset so we can test for no convertable columns above */
				return true;
			}

			/* If we have a work table, do we have a PKEY and a current row? */
			if ( \IPSUtf8\Session::i()->current_pkey )
			{
				$start = static::$utf->select('MAX(`' . \IPSUtf8\Session::i()->current_pkey . '`) as max', \IPSUtf8\Session::i()->current_table )->setKeyField('max')->first();

				$rows = static::$db->select(
							'*',
							$table['name'],
							array( \IPSUtf8\Session::i()->current_pkey . ' > ?', intval( $start ) ),
							\IPSUtf8\Session::i()->current_pkey . ' ASC',
							( $limit ? array( 0, ( $limit - $convertCount ) ) : null )
						);
			}
			else
			{
				$start = static::$utf->select('COUNT(*) as count', \IPSUtf8\Session::i()->current_table )->setKeyField('count')->first();

				/* Fetch via offset */
				$rows = static::$db->select(
							'*',
							$table['name'],
							null,
							null,
							( $limit ? array( $start, ( $limit - $convertCount ) ) : array( $start, 18446744073709551615 ) ) # No really, this is what MySQL recommends for offset, no limit (http://dev.mysql.com/doc/refman/5.1/en/select.html#id4651990)
						);
			}

			if ( !defined( 'FORCE_CONVERT_METHOD' ) OR FORCE_CONVERT_METHOD === NULL )
			{
				\IPSUtf8\Text\Charset::$method = 'internal';

				/* Optimise for latin character sets */
				if ( function_exists( 'mb_convert_encoding' ) )
				{
					if ( in_array( strtolower( $currentCharSet ), array_map( 'strtolower', mb_list_encodings() ) ) )
					{
						\IPSUtf8\Text\Charset::$method = 'mb';
					}
				}
			}

			$rowCount   = 0;
			$gotRows    = count( $rows );
			$batch      = array();
			$batchBytes = 0;
			$max        = ( $limit ) ?: 250;

			/* If we have a text column then the data set gets large */
			if ( $limit > 50 AND static::hasTextColumn( $table['name'] ) )
			{
				$max = 50;
			}

			/* Throttle inserts for other reasons? */
			$throttle = static::throttleInserts( $table['name'] );

			if ( $throttle !== false )
			{
				$max = ( $throttle < $limit ) ? $throttle : $limit;
			}

			/* Anything fetched? */
			if ( $gotRows === 0 )
			{
				try
				{
					static::$utf->query("ALTER TABLE `" . static::$utf->prefix . "{$table['name']}` ENABLE KEYS;");
					static::log( "{$table['name']} keys enabled." );
				}
				catch( \IPSUtf8\Db\Exception $e )
				{
					static::log( static::$utf->prefix . "{$table['name']} enable keys exception caught." );
				}
				
				$table        = $this->getNextTable( $table['name'] );
				$convertCols  = static::getConvertableColumns( $table['name'] );
				$numericCols  = static::getNumericColumns( $table['name'] );

				continue;
			}

			foreach( $rows as $row )
			{
				foreach( $convertCols as $col )
				{
					if ( isset( $row[ $col ] ) AND ! empty( $row[ $col ] ) )
					{
						if ( static::isSerialized( $row[ $col ] ) AND \IPSUtf8\Text\Charset::i()->needsConverting( $row[ $col ], $currentCharSet, 'UTF-8' ) )
						{
							/* Store a copy in case it doesn't work */
							$original = $row[ $col ];
							$work = unserialize( $row[ $col ] );

							array_walk_recursive( $work, function( &$input, $key ) use ($currentCharSet)
							{
								$input = \IPSUtf8\Text\Charset::i()->convert( $input, $currentCharSet, 'UTF-8' );
							} );

							$row[ $col ] = serialize( $work );
							
							/* Did it work? */
							if ( ! @unserialize( $row[ $col ] ) )
							{
								/* Something went wrong... maybe we can figure it out */
								$work = unserialize( $original );
								array_walk_recursive( $work, function ( &$input, $key ) use ( $currentCharSet, $tableCharSet ) {
									if ( function_exists( 'mb_detect_encoding' ) )
									{
										$encoding = mb_detect_encoding( $input );
										$input = \IPSUtf8\Text\Charset::i()->convert( $input, $encoding, 'UTF-8' );
									}
									else
									{
										/* @todo expand */
										if ( $tableCharSet == 'latin1' )
										{
											$encoding = 'ISO-8859-1';
										}
										$input = \IPSUtf8\Text\Charset::i()->convert( $input, $encoding, 'UTF-8' );
									}
								} );
								
								$row[ $col ] = serialize( $work );
							}
						}
						else
						{
							$row[ $col ] = \IPSUtf8\Text\Charset::i()->convert( $row[ $col ], $currentCharSet, 'UTF-8' );
						}
					}
				}

				/* Numeric columns */
				foreach( $numericCols as $col )
				{
					if ( isset( $row[ $col ] ) AND ! empty( $row[ $col ] ) )
					{
						if ( $row[ $col ] === null OR ! is_numeric( $row[ $col ] ) )
						{
							$row[ $col ] = 0;
						}
					}
				}

				/* Roughly add the size of this insert in bytes */
				$tmp = $row; // Use a copy so the reference in array_walk doesn't overwrite the row to be inserted

				/* Make sure it's an associative array as topic_views only has 1 column so DB driver returns an indexed array only */
				array_walk_recursive( $tmp, function( &$input, $key )
				{
					$input = htmlentities( (string) $input, ENT_QUOTES | ENT_IGNORE, 'utf-8', false );
				} );

				$batchBytes += mb_strlen( @json_encode( $tmp ), '8bit');
				unset( $tmp );
				$batch[]     = $row;
				$nextBatch   = null;

				/* Give us an error margin to account for MySQL syntax */
				if ( $batchBytes > ( $this->_data['max_allowed_packet'] - ( ( $this->_data['max_allowed_packet'] / 100 ) * ( count( $batch ) * 0.3 ) ) ) )
				{
					/* Remove last row */
					$nextBatch = array_pop( $batch );

					static::log( "Max packet hit with " . $batchBytes . 'b with ' . count( $batch ) . ' rows' );
				}
				
				/* If we have moved the only row from $batch to $nextbatch, then process that now */
				if ( count( $nextBatch ) and ! count( $batch ) )
				{
					$batch     = $nextBatch;
					$nextBatch = array();
				}
				
				/* Got a batch to write? */
				if ( $nextBatch !== null OR ( count( $batch ) === $max ) OR ( $gotRows === count( $batch ) ) )
				{
					try
					{
						/* Optimise */
						$this->preInsert( $table );

						$insertId = static::$utf->insert( $table['name'], $batch, false, true, $table );

						if ( ! empty( static::$utf->error ) )
						{
							throw new \RuntimeException( static::$utf->error );
						}

						$this->postInsert( $table );

						/* Update counts */
						$sessionData['convertedCount'] += count( $batch );
						$convertCount += count( $batch );

						if ( \IPSUtf8\Session::i()->current_pkey )
						{
							\IPSUtf8\Session::i()->current_row = $insertId;
						}
						else
						{
							\IPSUtf8\Session::i()->current_row += count( $batch );
						}

						\IPSUtf8\Session::i()->status = 'processing';
						\IPSUtf8\Session::i()->json   = $sessionData;
						\IPSUtf8\Session::i()->save();

						static::log( count( $batch ) . " rows batch inserted using conversion method: " . \IPSUtf8\Text\Charset::$method . ', last insert ID ' . $insertId );

						$batch      = ( $nextBatch !== null ) ? array( $nextBatch ) : array();
						$batchBytes = 0;
					}
					catch( \IPSUtf8\Db\Exception $e )
					{
						$msg = $e->getMessage();

						\IPSUtf8\Session::i()->save();

						/* Trying to insert duplicate data, just let it go to the next in case there is a small overlap */
						if ( mb_stristr( $msg, 'duplicate entry' ) )
						{
							static::log( "Duplicate key failure on insert\n{$msg}\n" . var_export( $batch, true ) );
							return $this->heal( $limit, $convertCount );
						}
					}
				}
			}

			/* Anything fetched? */
			if ( $gotRows === 0 )
			{
				try
				{
					static::$utf->query("ALTER TABLE `" . static::$utf->prefix . "{$table['name']}` ENABLE KEYS;");
					static::log( "{$table['name']} keys enabled." );
				}
				catch( \IPSUtf8\Db\Exception $e )
				{
					static::log( static::$utf->prefix . "{$table['name']} enable keys exception caught." );
				}
				
				$table       = $this->getNextTable( $table['name'] );
				$convertCols = static::getConvertableColumns( $table['name'] );
			}
		}

		return true;
	}


	/**
	 * Pre insert
	 *
	 * @param	array	$table	Table definition data
	 * @return void
	 */
	public function preInsert( $table )
	{
		$row = static::$utf->query( "SHOW TABLE STATUS LIKE '" . static::$utf->prefix . $table['name'] . "'" )->fetch_assoc();
		static::$utfTableEngine	= $row['Engine'];

		if ( empty( static::$utfTableEngine ) )
		{
			static::$utfTableEngine = static::$utf->defaultEngine();
		}

		if ( \strtolower( static::$utfTableEngine ) === 'myisam' )
		{
			static::log( "Pre inserts for MyISAM table " . $table['name'] );

			try
			{
				//static::$utf->query("ALTER TABLE " . static::$utf->prefix . "{$table['name']} DISABLE KEYS;");
			}
			catch( \IPSUtf8\Db\Exception $e )
			{
				static::log( static::$utf->prefix . "{$table['name']} disable keys exception caught." );
			}
		}
		else if ( \strtolower( static::$utfTableEngine ) === 'innodb' )
		{
			static::log( "Pre inserts for InnoDB table " . $table['name'] );

			try
			{
				static::$utf->query("SET autocommit=0");
				static::$utf->query("SET unique_checks=0");
				static::$utf->query("SET foreign_key_checks=0");
			}
			catch ( Exception $e ) { }
		}
	}

	/**
	 * Post insert
	 *
	 * @param	array	$table	Table definition data
	 * @return void
	 */
	public function postInsert( $table )
	{
		if ( \strtolower( static::$utfTableEngine ) === 'myisam' )
		{
			static::log( "Post inserts for MyISAM table " . $table['name'] );

			try
			{
				//static::$utf->query("ALTER TABLE " . static::$utf->prefix . "{$table['name']} ENABLE KEYS;");
			}
			catch( \IPSUtf8\Db\Exception $e )
			{
				static::log( static::$utf->prefix . "{$table['name']} enable keys exception caught." );
			}
		}
		else if ( \strtolower( static::$utfTableEngine ) === 'innodb' )
		{
			static::log( "Post inserts for InnoDB table " . $table['name'] );

			try
			{
				static::$utf->query("SET autocommit=1");
				static::$utf->query("SET unique_checks=1");
				static::$utf->query("SET foreign_key_checks=1");
			}
			catch ( Exception $e ) { }
		}
	}

	/**
	 * Finish the conversion
	 *
	 * @return null
	 */
	public function finish()
	{
		$charset   = 'utf8';
		$collation = 'utf8_unicode_ci';

		/* MB4? */
		$sessionData = \IPSUtf8\Session::i()->json;

		if ( isset( $sessionData['use_utf8mb4'] ) AND ! empty( $sessionData['use_utf8mb4'] ) )
		{
			static::$utf->set_charset( 'utf8mb4' );

			$charset   = 'utf8mb4';
			$collation = 'utf8mb4_unicode_ci';
		}

		/* IPB 3? */
		if ( isset( \IPSUtf8\Session::i()->tables['core_sys_conf_settings'] ) AND isset( \IPSUtf8\Session::i()->tables['cache_store'] ) )
		{
			if ( static::$utf->checkForTable('core_sys_conf_settings' ) )
			{
				static::$utf->update( 'core_sys_conf_settings', array( 'conf_value' => '' ), array( 'conf_key=?', 'gb_char_set' ) );

				$rows = static::$utf->select( '*', 'core_sys_conf_settings', array( 'conf_add_cache=?', 1 ) );

				$settings = array();
				foreach( $rows as $row ) #row your boat
				{
					$value = $row['conf_value'] != "" ?  $row['conf_value'] : $row['conf_default'];

					if ( $value == '{blank}' )
					{
						$value = '';
					}

					$settings[ $row['conf_key'] ] = $value;
				}

				static::$utf->update( 'cache_store', array( 'cs_value' => serialize( $settings ) ), array( 'cs_key=?', 'settings' ) );
			}

			/* Update Language Locales */
			if ( static::$utf->checkForTable('core_sys_lang') )
			{
				/* Store current */
				$currentLocale = setlocale( LC_ALL, '0' );

				/* Loop through languages */
				$languages	= static::$utf->select( '*', 'core_sys_lang' );
				foreach( $languages AS $language )
				{
					$locale = explode( '.', $language['lang_short'] ); # We want to update even if a charset is already set.
					foreach( array( "{$locale[0]}.UTF8", "{$locale[0]}.UTF-8", "{$locale[0]}.utf8" ) AS $test )
					{
						$verify = setlocale( LC_ALL, $test );

						if ( $verify !== FALSE )
						{
							static::$utf->update( 'core_sys_lang', array( 'lang_short' => $test ), array( 'lang_id=?', $language['lang_id'] ) );
							break;
						}
					}
				}

				/* Clear Cache so it regenerates */
				static::$utf->update( 'cache_store', array( 'cs_value' => '' ), array( 'cs_key=?', 'lang_data' ) );
			}

			/* Unlock Tasks */
			if( static::$utf->checkForTable('task_manager') )
			{
				static::$utf->update( "task_manager", array( 'task_locked' => 0 ) );
			}
			else if( static::$utf->checkForTable('core_tasks') )
			{
				static::$utf->update( "core_tasks", array( 'next_run' => time() ) );
			}
		}

		/* Change database collation */
		require( ROOT_PATH . '/conf_global.php' );

		static::$utf->query( "ALTER DATABASE `" . $INFO['sql_database'] . "` DEFAULT CHARACTER SET " . $charset . " COLLATE " . $collation );
	}

	/**
	 * Restore the original tables.
	 *
	 * @return null
	 */
	public function restoreOriginalTables()
	{
		/* Grab all tables to convert */
		$stmt = static::$db->prepare( "SHOW TABLES" );
		$stmt->execute();
		$stmt->bind_result( $name );

		$tables = array();

		while ( $stmt->fetch() === true )
		{
			$tables[] = $name;
		}

		foreach( $tables as $name )
		{
			if ( mb_substr( $name, 0, 5 ) === 'orig_' )
			{
				$plainName = mb_substr( $name, 5 );

				static::$db->query( "DROP TABLE IF EXISTS `" . $plainName . "`" );

				$rename = "RENAME TABLE `" . $name . '` TO `' . $plainName . '`';

				static::$db->query( $rename );

				static::log( $rename );
			}
		}
	}

	/**
	 * Delete the original tables.
	 *
	 * @return null
	 */
	public function deleteOriginalTables()
	{
		/* Grab all tables to convert */
		$stmt = static::$utf->prepare( "SHOW TABLES" );
		$stmt->execute();
		$stmt->bind_result( $name );

		$tables = array();

		while ( $stmt->fetch() === true )
		{
			$tables[] = $name;
		}

		foreach( $tables as $name )
		{
			if ( mb_substr( $name, 0, 5 ) === 'orig_' )
			{
				static::$db->query( "DROP TABLE IF EXISTS `" . $name . "`" );

				static::log( "DROP TABLE IF EXISTS `" . $name . "`" );
			}
		}
	}

	/**
	 * Rename the tables
	 *
	 * @return null
	 */
	public function renameTables()
	{
		/* Grab all tables to convert */
		$stmt = static::$db->prepare( "SHOW TABLES" );
		$stmt->execute();
		$stmt->bind_result( $name );

		$tables     = array();
		$origPrefix = static::$db->prefix;

		if ( mb_substr( $origPrefix, 0, 6 ) === 'x_utf_' )
		{
			$origPrefix = mb_substr( $origPrefix, 6 );
		}

		while ( $stmt->fetch() === true )
		{
			if( mb_substr( $name, 0, 6 ) === 'x_utf_' )
			{
				$tables[] = $name;
			}
		}

		foreach( $tables as $name )
		{
			$tableNameNoPrefix = $name;
			$tableName = mb_substr( $name, mb_strlen( static::$utf->prefix ) );

			if ( $tableName !== 'convert_session' AND $tableName !== 'convert_session_tables' )
			{
				if( static::$utf->checkForTable( $tableName ) )
				{
					static::$db->query( "DROP TABLE IF EXISTS `" . $origPrefix . $tableName . '`' );
					static::log( "DROP TABLE IF EXISTS `" . $origPrefix . $tableName . '`' );
				}

				static::$utf->query( "RENAME TABLE `" . $tableNameNoPrefix . '` TO `' . $origPrefix . $tableName . '`' );
				static::log( "RENAME TABLE `" . $tableNameNoPrefix . '` TO `' . $origPrefix . $tableName . '`' );
			}
		}

		/* Update table data */
		\IPSUtf8\Session::i()->updateTableData();
	}

	/**
	 * Go through the DB and fix the collation of UTF8 tables
	 *
	 * @return null
	 */
	public function fixCollation()
	{
		$charset   = 'utf8';
		$collation = 'utf8_unicode_ci';

		/* MB4? */
		$sessionData = \IPSUtf8\Session::i()->json;

		if ( isset( $sessionData['use_utf8mb4'] ) AND ! empty( $sessionData['use_utf8mb4'] ) )
		{
			static::$utf->set_charset( 'utf8mb4' );

			$charset   = 'utf8mb4';
			$collation = 'utf8mb4_unicode_ci';
		}

		/* Change database collation */
		require( ROOT_PATH . '/conf_global.php' );

		static::$utf->query( "ALTER DATABASE `" . $INFO['sql_database'] . "` DEFAULT CHARACTER SET " . $charset . " COLLATE " . $collation );

		foreach( \IPSUtf8\Session::i()->tables as $name => $data )
		{
			if ( in_array( $name, $this->getNonUtf8CollationTables() ) )
			{
				/* Skip if it's an _old table */
				if( mb_substr( $name, -4 ) == '_old' )
				{
					continue;
				}

				$cols      = static::getConvertableColumns( $data['name'] );
				$tableData = \IPSUtf8\Session::i()->tables[ $data['name'] ];

				if ( count( $cols ) )
				{
					$dropIndexes    = array();
					$addIndexes     = array();
					$modify         = array();
					$hasUnique		= FALSE;

					/* Changing collation can cause issues for duplicate enries in unique keys */
					if ( isset( $tableData['definition']['indexes'] ) )
					{
						foreach( $tableData['definition']['indexes'] as $key => $index )
						{
							if ( $index['type'] === 'unique' )
							{
								/* Make sure none of the columns in this index are auto_increment */
								foreach( $index['columns'] AS $k => $idxcol )
								{
									if ( $tableData['definition']['columns'][ $idxcol ]['auto_increment'] === TRUE )
									{
										continue 2;
									}
								}

								if ( static::$db->checkForIndex( $name, $key ) )
								{
									$hasUnique = TRUE;
									break;
								}
							}
						}
					}
				}

				/* We don't need to drop any indexes - just create a copy of the table and insert ignore */
				if ( $hasUnique === TRUE )
				{
					static::$utf->query( "RENAME TABLE `" . static::$db->prefix . $name . "` TO `" . static::$db->prefix . $name . "_temp`" );
					static::$utf->query( "CREATE TABLE `" . static::$db->prefix . $name . "` LIKE `" . static::$db->prefix . $name . "_temp`" );
					static::$utf->query( "INSERT IGNORE INTO `" . static::$db->prefix . $name . "` SELECT * FROM `" . static::$db->prefix . $name . "_temp`" );
					static::$utf->query( "DROP TABLE `" . static::$db->prefix . $name . "_temp`" );
				}

				static::$utf->query( "ALTER TABLE `" . static::$db->prefix . $name . "` CONVERT TO CHARACTER SET " . $charset . " COLLATE " . $collation );

				if ( count( $cols ) )
				{
					foreach( $cols as $col )
					{
						if ( isset( $tableData['definition']['columns'][ $col ] ) )
						{
							$colData = $tableData['definition']['columns'][ $col ];

							$modify[] = " MODIFY `" . $col . "` " .  $colData['type'] . ( ( is_numeric( $colData['length'] ) AND $colData['length'] > 0 ) ? "(" . $colData['length'] . ")" : '' ) . " CHARACTER SET " . $charset . " COLLATE " . $collation;
						}
					}
				}

				if ( count( $cols ) )
				{
					if ( count( $modify ) )
					{
						$query = "ALTER TABLE `" . static::$db->prefix . $name . "` " . implode( ',', $modify );

						static::log( $query );
						static::$utf->query( $query );
					}

					if ( /*count( $modify ) and*/ count( $addIndexes ) )
					{
						$query = "ALTER TABLE `" . static::$db->prefix . $name . "` " . implode( ',', $addIndexes );

						static::log( $query );
						static::$utf->query( $query );
					}
				}
			}
		}

		/* Update table data */
		\IPSUtf8\Session::i()->updateTableData();
	}

	/**
	 * Something went wrong, so try and heal to restart progress
	 *
	 * @param	int		$limit			Process() method limit
	 * @param	int		$cycleCount		Items processed if this was in the middle of process()
	 * @return	void
	 */
	public function heal( $limit=250, $cycleCount=0 )
	{
		if ( \IPSUtf8\Session::i()->current_table )
		{
			$table = \IPSUtf8\Session::i()->tables[ \IPSUtf8\Session::i()->current_table ];

			/* Table doesn't exist, so rewind back to the start of this table */
			if ( ! static::$utf->checkForTable( $table['name'] ) )
			{
				if ( static::$utf->createTable( $table['definition'] ) === false )
				{
					throw new \RuntimeException( static::$utf->error . "\n" . var_export( $table['definition'], true ) );
				}

				$completed = \IPSUtf8\Session::i()->completed_json;

				if ( isset( $completed[ \IPSUtf8\Session::i()->current_table ] ) )
				{
					unset( $completed[ \IPSUtf8\Session::i()->current_table ] );
				}

				/* Got a primary key so we can use a WHERE N > X query rather than a limit for efficiency? */
				if ( isset( $table['definition']['indexes']['PRIMARY'] ) )
				{
					$pkey = $table['definition']['indexes']['PRIMARY']['columns'][0];

					/* Is it numeric? */
					if ( mb_stristr( $table['definition']['columns'][ $pkey ]['type'], 'int' ) )
					{
						\IPSUtf8\Session::i()->current_pkey = $pkey;
					}
				}

				\IPSUtf8\Session::i()->status         = 'processing';
				\IPSUtf8\Session::i()->completed_json = $completed;
				\IPSUtf8\Session::i()->current_row    = 0;
				\IPSUtf8\Session::i()->save();

				/* Run again */
				return $this->process( ( $limit - $cycleCount ) );
			}

			if ( \IPSUtf8\Session::i()->current_pkey )
			{
				/* Get the latest row */
				$max = static::$utf->select('MAX(`' . \IPSUtf8\Session::i()->current_pkey . '`) as max', \IPSUtf8\Session::i()->current_table )->setKeyField('max')->first();
			}
			else
			{
				/* Get the count */
				$max = static::$utf->select('COUNT(*) as count', \IPSUtf8\Session::i()->current_table )->setKeyField('count')->first();
			}

			\IPSUtf8\Session::i()->status      = 'processing';
			\IPSUtf8\Session::i()->current_row = $max;
			\IPSUtf8\Session::i()->save();

			/* Run again */
			return $this->process( ( $limit - $cycleCount ), TRUE );
		}
	}

	/**
	 * Set the current work table
	 *
	 * @return void
	 */
	public function getTable()
	{
		$table = null;

		if ( \IPSUtf8\Session::i()->current_table AND isset( \IPSUtf8\Session::i()->tables[ \IPSUtf8\Session::i()->current_table ] ) )
		{
			/* Table selected and convertion in progress */
			$table = \IPSUtf8\Session::i()->tables[ \IPSUtf8\Session::i()->current_table ];

			static::log( "Continuing with " . $table['name'] . ' (PKEY: ' . \IPSUtf8\Session::i()->current_pkey . ')' );

			if ( ! static::$utf->checkForTable( $table['name'] ) )
			{
				if ( static::$utf->createTable( $this->checkTable( $table['definition'] ) ) === false )
				{
					throw new \RuntimeException( static::$utf->error . "\n" . var_export( $table['definition'], true ) );
				}
				try
				{
					static::$utf->query("ALTER TABLE `" . static::$utf->prefix . "{$table['name']}` DISABLE KEYS;");
					static::log( "{$table['name']} keys disabled." );
				}
				catch( \IPSUtf8\Db\Exception $e )
				{
					static::log( static::$utf->prefix . "{$table['name']} disable keys exception caught." );
				}
			}

			return $table;
		}
		else
		{
			/* need to resolve an issue where if you only need to convert a few tables, these few will be named x_utf_ while others won't,
			   so for now, just convert all */
			$nonUtf8Tables = array_keys( \IPSUtf8\Session::i()->tables );//$this->getNonUtf8Tables();

			/* No table selected */
			if ( is_array( $nonUtf8Tables ) and count( $nonUtf8Tables ) )
			{
				if ( is_array( \IPSUtf8\Session::i()->completed_json ) )
				{
					$diff = array_diff( $nonUtf8Tables, array_keys( \IPSUtf8\Session::i()->completed_json ) );

					if ( count( $diff ) )
					{
						$table = \IPSUtf8\Session::i()->tables[ array_shift( $diff ) ];
					}
					else
					{
						return null;
					}
				}
				else
				{
					$table = array_shift( $nonUtf8Tables );
				}

				if ( isset( $table['name'] ) )
				{
					\IPSUtf8\Session::i()->current_table = $table['name'];

					/* Got a primary key so we can use a WHERE N > X query rather than a limit for efficiency? */
					if ( isset( $table['definition']['indexes']['PRIMARY'] ) )
					{
						$pkey = $table['definition']['indexes']['PRIMARY']['columns'][0];

						/* Is it numeric? */
						if ( mb_stristr( $table['definition']['columns'][ $pkey ]['type'], 'int' ) )
						{
							\IPSUtf8\Session::i()->current_pkey = $pkey;
						}
					}

					\IPSUtf8\Session::i()->save();

					/* Create the table for UTF8 goodness */
					static::$utf->dropTable( $table['name'], true );

					if ( static::$utf->createTable( $this->checkTable( $table['definition'] ) ) === false )
					{
						throw new \RuntimeException(  static::$utf->error . "\n" . var_export( $table['definition'], true ) );
					}

					static::log( "Created UTF8 table " . $table['name'] . ' (PKEY: ' . \IPSUtf8\Session::i()->current_pkey . ')' );

					try
					{
						static::$utf->query("ALTER TABLE `" . static::$utf->prefix . "{$table['name']}` DISABLE KEYS;");
						static::log( "{$table['name']} keys disabled." );
					}
					catch( \IPSUtf8\Db\Exception $e )
					{
						static::log( static::$utf->prefix . "{$table['name']} disable keys exception caught." );
					}

					return $table;
				}
			}
		}

		return null;
	}

	/**
	 * Attempt to fix issues with keys longer than 1000bytes
	 *
	 * @param	array	$definition		Table definition
	 * @return	array
	 */
	public function checkTable( $definition )
	{
		/* MB4? */
		$sessionData = \IPSUtf8\Session::i()->json;
		$length      = 0;
		$multiplier  = ( ! empty( $sessionData['use_utf8mb4'] ) ) ? 4 : 3;
		$needsFixing = array();
		$maxLen      = 1000;

		if ( \mb_strtolower( $definition['engine'] ) === 'innodb' )
		{
			$maxLen = 767;
		}

		if ( isset( $definition['indexes'] ) )
		{
			foreach( $definition['indexes'] as $key => $index )
			{
				foreach( $index['columns'] as $i => $column )
				{
					if ( isset( $definition['columns'][ $column ] ) and ( ( ! empty( $definition['columns'][ $column ]['length'] ) or in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'longtext', 'mediumtext', 'text' ) ) ) ) )
					{
						$length += (int) ( $definition['columns'][ $column ]['length'] ) ? $definition['columns'][ $column ]['length'] : 250;
					}
				}

				if ( $length * $multiplier > $maxLen )
				{
					foreach( $index['columns'] as $i => $column )
					{
						if ( isset( $definition['columns'][ $column ] ) and ( ( ! empty( $definition['columns'][ $column ]['length'] ) or in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'longtext', 'mediumtext', 'text' ) ) ) ) )
						{
							/* Column name, column length, column type  */
							$needsFixing[ $key ][ $i ] = array( $column, ( (int) $definition['columns'][ $column ]['length'] ) ? $definition['columns'][ $column ]['length'] : 250, $definition['columns'][ $column ]['type'] );
						}
					}
				}

				$length = 0;
			}
		}

		if ( count( $needsFixing ) )
		{
			foreach( $needsFixing as $key => $i )
			{
				$totalLength	= 0;
				$maxChars		= $maxLen / $multiplier;

				foreach( $i as $vals )
				{
					$totalLength += $vals[1];
				}

				if ( $totalLength > $maxChars )
				{
					/* Check each column can be reduced by the amount we need reducing */
					$debt = 0;
 
					$reduceEachBy = ( ( 100 / $totalLength ) * $maxChars ) / 100;

					/* Apply debt if we have any. We do not reduce integers */
					foreach( $i as $x => $vals )
					{
						if ( array_key_exists( mb_strtolower( $vals[2] ), static::$numericCols ) AND static::$numericCols[ mb_strtolower( $vals[2] ) ] !== NULL )
						{
							/* Don't go by "display" space, go by storage space, which is static regardless of supplied value ... an INT(4) still uses 11 chars */
							$debt += static::$numericCols[ mb_strtolower( $vals[2] ) ];
						}
					}

					/* Recalculate value to multiply index sub lengths with (subtracting debt) */
					$reduceEachBy = ( ( 100 / ( $totalLength - $debt ) ) * ( $maxChars - $debt ) ) / 100;

					foreach( $i as $x => $vals )
					{
						/* No length? */
						if ( empty( $vals[1] ) )
						{
							$vals[1] = 250;
						}

						if ( array_key_exists( mb_strtolower( $vals[2] ), static::$numericCols ) AND static::$numericCols[ mb_strtolower( $vals[2] ) ] !== NULL )
						{
							/* Preserve col len */
							continue;
						}

						$vals[1] = floor( $vals[1] * $reduceEachBy );	
						$i[ $x ] = $vals;
					}
				}

				foreach( $i as $x => $vals )
				{
					if ( $definition['columns'][ $definition['indexes'][ $key ]['columns'][ $x ] ]['length'] != $vals[1] )
					{
						$definition['indexes'][ $key ]['length'][ $x ] = $vals[1];
					}
				}
			}
		}

		return $definition;
	}

	/**
	 * Fetches the next table to process or false if nothing left to process
	 *
	 * @param	string	$currentTable		Name of table just processed
	 * @return	array	Array of table data
	 */
	public function getNextTable( $currentTable )
	{
		try
		{
			static::$utf->query("ALTER TABLE `" . static::$utf->prefix . "{$currentTable}` ENABLE KEYS;");
			static::log( "{$table['name']} keys enabled." );
		}
		catch( \IPSUtf8\Db\Exception $e )
		{
			static::log( static::$utf->prefix . "{$currentTable} enable keys exception caught." );
		}
		
		$completed   				= \IPSUtf8\Session::i()->completed_json;
		$completed[ $currentTable ] = $currentTable;

		\IPSUtf8\Session::i()->completed_json = $completed;
		\IPSUtf8\Session::i()->current_pkey   = null;
		\IPSUtf8\Session::i()->current_table  = null;
		\IPSUtf8\Session::i()->current_row    = 0;

		\IPSUtf8\Session::i()->save();

		return $this->getTable();
	}

	/**
	 * Init this class
	 */
	 public function init()
	 {
		/* Grab all tables to convert */
		$stmt = static::$db->query( "show variables" );

		$tables = array();

		while ( $row = $stmt->fetch_array( MYSQLI_ASSOC ) )
		{
			$key   = $row['Variable_name'];
			$value = $row['Value'];

			if ( $key == 'character_set_database' )
			{
				$this->_data['database_charset'] = strtolower( $value );
			}

			if ( $key == 'bulk_insert_buffer_size' )
			{
				$this->_data['bulk_insert_buffer_size'] = $value;
			}

			if ( $key == 'max_allowed_packet' )
			{
				$this->_data['max_allowed_packet'] = $value;
			}
		}

		if ( ! empty( $this->_data['max_allowed_packet'] ) AND ! empty( $this->_data['bulk_insert_buffer_size'] ) AND ( $this->_data['bulk_insert_buffer_size'] < $this->_data['max_allowed_packet'] ) )
		{
			$this->_data['max_allowed_packet'] = $this->_data['bulk_insert_buffer_size'];
		}

		if ( empty( $this->_data['max_allowed_packet'] ) AND empty( $this->_data['bulk_insert_buffer_size'] ) )
		{
			/* No value so use MySQL default of 1MB */
			$this->_data['max_allowed_packet'] = 1048576;
		}
	}

	/**
	 * Returns true if the table has a text or medium text column
	 *
	 * @param	string	$name		Name of table
	 * @return	boolean
	 */
	public static function hasTextColumn( $name )
	{
		$table = \IPSUtf8\Session::i()->tables[ $name ];

		foreach( $table['definition']['columns'] as $col => $val )
		{
			if ( in_array( mb_strtolower( $val['type'] ), array( 'mediumtext', 'text' ) ) )
			{
				return true;
			}
		}

		return false;
	}


	/**
	 * Tables with lots of dynamic columns can be slow
	 *
	 * @param	string	$name		Name of table
	 * @return	boolean
	 */
	public static function throttleInserts( $name )
	{
		$table = \IPSUtf8\Session::i()->tables[ $name ];
		$total = 0;
		$var   = 0;

		foreach( $table['definition']['columns'] as $col => $val )
		{
			$total++;

			if ( in_array( mb_strtolower( $val['type'] ), array( 'varchar' ) ) )
			{
				$var++;
			}
		}

		if ( $total > 70 )
		{
			return 50;
		}
		else if ( $total > 30 AND $var > 0 )
		{
			if ( ( $var / $total ) * 100 > 25 )
			{
				return 100;
			}
		}

		return false;
	}

	/**
	 * Returns whether the DB is UTF8 already
	 *
	 * @return 	bool
	 */
	public function databaseIsUtf8()
	{
		$yeahProbably = false;

		if ( \IPSUtf8\Convert::i()->database_charset == 'utf8mb4' OR \IPSUtf8\Convert::i()->database_charset == 'utf8' OR \IPSUtf8\Session::i()->current_charset == 'utf-8' )
		{
			$yeahProbably = true;
		}

		/* Another check */
		if ( $yeahProbably === true )
		{
			/* Best check the tables, then */
			$json = \IPSUtf8\Session::i()->json;

			if ( count( $this->getNonUtf8Tables() ) )
			{
				$yeahProbably = false;
			}
		}

		return $yeahProbably;
	}

	/**
	 * Grab the character sets from MySQL
	 *
	 * @return array
	 */
	public function getMysqlCharSets()
	{
		if ( static::$mysqlCharSets === NULL )
		{
			static::$mysqlCharSets = array( 'latin1', 'utf8' );

			$stmt = static::$db->query( "show character set" );

			while ( $row = $stmt->fetch_array( MYSQLI_ASSOC ) )
			{
				static::$mysqlCharSets[] = $row['Charset'];
			}
		}

		return static::$mysqlCharSets;
	}

	/**
	 * Return tables that need converting
	 *
	 * @param	boolean	$force	Force a recount
	 * @return array
	 */
	public function getNonUtf8Tables( $force=false )
	{
		if ( static::$nonUtf8Tables === NULL or $force === TRUE )
		{
			static::$nonUtf8Tables = array();

			/* Best check the tables, then */
			$json = \IPSUtf8\Session::i()->json;

			foreach( $json['charSets'] as $charSet => $tablesArray )
			{
				if ( ! empty( $json['force_conversion'] ) or ( $charSet != 'utf8' and $charSet != 'utf8mb4' ) )
				{
					static::$nonUtf8Tables = array_merge( static::$nonUtf8Tables, $tablesArray );
				}
			}
		}

		return static::$nonUtf8Tables;
	}

	/**
	 * Returns total rows to convert
	 *
	 * @return array
	 */
	public function getTotalRowsToConvert()
	{
		if ( static::$rowsToConvert === NULL )
		{
			static::$rowsToConvert = 0;

			foreach( array_keys( \IPSUtf8\Session::i()->tables ) as $table )
			{
				static::$rowsToConvert += intval( \IPSUtf8\Session::i()->tables[ $table ]['count'] );
			}
		}

		return static::$rowsToConvert;
	}

	/**
	 * Returns whether the DB has the correct collation
	 *
	 * @return 	bool
	 */
	public function getNonUtf8CollationTables()
	{
		if ( static::$nonUtf8Collations === null )
		{
			static::$nonUtf8Collations = array();

			/* Best check the tables, then */
			foreach( \IPSUtf8\Session::i()->tables as $table => $definition )
			{
				/* If the table itself is not utf8_unicode_ci, don't bother check columns */
				if ( $definition['collation'] AND !in_array( $definition['collation'], array( 'utf8_unicode_ci', 'utf8mb4_unicode_ci' ) ) )
				{
					static::$nonUtf8Collations[] = $table;
					continue;
				}

				foreach( $definition['definition']['columns'] as $name => $column )
				{
					if ( $column['collation'] and ! in_array( $column['collation'], array( 'utf8_unicode_ci', 'utf8mb4_unicode_ci' ) ) )
					{
						static::$nonUtf8Collations[] = $table;
						break;
					}
				}
			}
		}

		return static::$nonUtf8Collations;
	}

	/**
	 * Fetch debug information
	 *
	 * @return array
	 */
	public function getDebugString()
	{
		$data           = array();
		$tableCharSet   = \IPSUtf8\Convert::i()->database_charset;
		$currentCharSet = \IPSUtf8\Session::i()->current_charset;

		$json = \IPSUtf8\Session::i()->json;
		$data[] = "IP.Board Character Set: " . $currentCharSet;
		$data[] = "Database Character Set: " . $tableCharSet;
		$data[] = "Original table prefix: " . static::$db->prefix;
		$data[] = "Converted table prefix: " . static::$utf->prefix;

		foreach( $json['charSets'] as $charSet => $tablesArray )
		{
			$data[] = count( $tablesArray ) . ' tables are ' . $charSet;
		}

		$data[] = count( $this->getNonUtf8CollationTables() ) . " tables have incorrect collations";
		$data[] = "Can use 'dump' method: " . var_export( $this->canDump(), true );

		return $data;
	}

	/**
	 * Detect whether this is a serialized string or not
	 *
	 * @param	string	$string	The actual string
	 * @return	boolean
	 */
	public static function isSerialized( $string )
	{
		/* If it looks nothing like a serialized string, then return it */
		if ( ! preg_match( '#^a:\d+:\{#', $string ) )
		{
			return false;
		}

		/* Now make sure it's actually a serialized string */
		return ( boolean ) @unserialize( $string );
	}

	/**
	 * Log data
	 */
	public static function log( $message )
	{
		$file  = THIS_PATH . '/tmp/log_' . date('Y-m-d') . '.cgi';
		$isNew = false;

		if ( ! is_file( $file ) )
		{
			$isNew = true;
		}

		@file_put_contents( $file, "\n" . str_repeat( '-', 48 ) . "\n" . date('r') . "\n" . $message, FILE_APPEND );

		if ( $isNew )
		{
			@chmod( $file, 0777 );
		}
	}

	/**
	 * Return a list of numeric columns that need checking
	 *
	 * @param	string	$name	Table
	 * @return	array	Array of columns that need checking as they can contain INT
	 */
	public static function getNumericColumns( $name )
	{
		if ( ! is_string( $name ) )
		{
			return false;
		}

		$table  = \IPSUtf8\Session::i()->tables[ $name ];
		$return = array();

		foreach( $table['definition']['columns'] as $col => $val )
		{
			if ( in_array( mb_strtolower( $val['type'] ), static::$numericCols ) )
			{
				$return[] = $val['name'];
			}
		}

		return $return;
	}

	/**
	 * Return a list of columns that need converting
	 *
	 * @param	string	$name	Table
	 * @return	array	Array of columns that need converting as they can contain text
	 */
	public static function getConvertableColumns( $name )
	{
		if ( ! is_string( $name ) )
		{
			return false;
		}

		$table  = \IPSUtf8\Session::i()->tables[ $name ];
		$return = array();

		foreach( $table['definition']['columns'] as $col => $val )
		{
			if ( in_array( mb_strtolower( $val['type'] ), static::$convertCols ) )
			{
				$return[] = $val['name'];
			}
		}

		return $return;
	}

	/**
	 * Can we use the fast dump method?
	 *
	 * @return boolean
	 */
	public static function canDump()
	{
		/* Best check the tables, then */
		$json     = \IPSUtf8\Session::i()->json;
		$tablesOk = true;

		foreach( $json['charSets'] as $charSet => $tablesArray )
		{
			if ( $charSet != 'utf8' and $charSet != 'utf8mb4' and mb_substr( $charSet, 0, 5) != 'latin' and $charSet != 'windows-1252' )
			{
				$tablesOk = false;
				break;
			}
		}

		if (
			( $tablesOk ) AND
			( \IPSUtf8\Session::i()->current_charset == 'utf-8' or \IPSUtf8\Session::i()->current_charset == 'iso-8859-1' ) AND
			( ( is_callable( 'exec' ) AND false === stripos( ini_get( 'disable_functions' ), 'exec' ) ) )
		)
		{
			try
			{
				@exec("iconv -l", $output );
			}
			catch( \ErrorException $e )
			{
				return false;
			}

			if ( ! array( $output ) or ! count( $output ) or ( ! stristr( implode( "\n", $output ), 'latin' ) ) )
			{
				return false;
			}

			return true;
		}
		else
		{
			return false;
		}
	}
	
	/**
	 * Get Table Schematic
	 *
	 * @return	array
	 */
	protected static function getTableSchematic( $table )
	{
		return json_decode( \IPSUtf8\Db::i('utf8')->select( 'table_schema', 'convert_session_tables', array( "table_name=?", $table ) )->first(), TRUE );
	}
}
