<?php

namespace SMW\Tests\Utils\Connection;

use CloneDatabase;
use MediaWiki\MediaWikiServices;
use RuntimeException;
use SMW\Connection\ConnectionProvider;
use SMW\Store;
use SMW\Tests\Utils\PageCreator;
use Wikimedia\Rdbms\IDatabase;

/**
 * @license GPL-2.0-or-later
 * @since 2.0
 *
 * @author mwjames
 */
class TestDatabaseTableBuilder {

	/**
	 * @var TestDatabaseTableBuilder
	 */
	private static $instance = null;

	/**
	 * @var Store
	 */
	protected $store;

	/**
	 * @var IConnectionProvider
	 */
	protected $connectionProvider;

	/**
	 * @var CloneDatabase
	 */
	protected $cloneDatabase;

	protected $supportedDatabaseTypes = [
		'mysql',
		'sqlite',
		'postgres'
	];

	protected $availableDatabaseTypes = [];

	private static $UTDB_PREFIX = null;
	private static $MWDB_PREFIX = null;

	private $dbSetup = false;

	/**
	 * @since 2.0
	 *
	 * @param Store $store
	 * @param IConnectionProvider $connectionProvider
	 */
	public function __construct( Store $store, ConnectionProvider $connectionProvider ) {
		$this->store = $store;
		$this->connectionProvider = $connectionProvider;
		$this->availableDatabaseTypes = $this->supportedDatabaseTypes;

		self::$UTDB_PREFIX = 'sunittest_';
		self::$MWDB_PREFIX = $GLOBALS['wgDBprefix'];

		// MediaWikiTestCase changes the GLOBAL state therefore
		// we have to ensure that we really use the original DB and
		// not an existing 'unittest_' created by a MW test
		if ( self::$MWDB_PREFIX === 'unittest_' ) {
			self::$MWDB_PREFIX = '';
		}
	}

	/**
	 * @since 2.0
	 *
	 * @param Store $store
	 *
	 * @return self
	 */
	public static function getInstance( Store $store ) {
		if ( self::$instance === null ) {
			self::$instance = new self( $store, new TestDatabaseConnectionProvider() );
		}

		return self::$instance;
	}

	/**
	 * @since 2.0
	 *
	 * @param string|null $databaseType
	 *
	 * @return self
	 */
	public function removeAvailableDatabaseType( $databaseType = null ) {
		$this->availableDatabaseTypes = array_diff( $this->supportedDatabaseTypes, (array)$databaseType );
		return $this;
	}

	/**
	 * @since 2.0
	 *
	 * @throws RuntimeException
	 */
	public function doBuild() {
		if ( !$this->isAvailableDatabaseType() ) {
			throw new RuntimeException( 'Requested DB type is not available through this installer' );
		}

		$this->setupDatabaseTables();
		$this->rollbackOpenDatabaseTransactions();
	}

	/**
	 * @since 2.0
	 */
	public function doDestroy() {
		$this->destroyDatabaseTables();
		$this->rollbackOpenDatabaseTransactions();
	}

	/**
	 * @since  2.0
	 *
	 * @return string
	 */
	public function getDBPrefix() {
		return self::$UTDB_PREFIX;
	}

	/**
	 * @since  2.0
	 *
	 * @return IDatabase
	 */
	public function getDBConnection() {
		return $this->connectionProvider->getConnection();
	}

	/**
	 * @since  2.0
	 *
	 * @return ConnectionProvider
	 */
	public function getConnectionProvider() {
		return $this->connectionProvider;
	}

	/**
	 * @see MediaWikiTestCase::listTables
	 */
	protected function generateListOfTables() {
		$dbConnection = $this->getDBConnection();

		$tables = $dbConnection->listTables(
			self::$MWDB_PREFIX,
			__METHOD__
		);

		// MW < 1.42
		if (
			version_compare( MW_VERSION, '1.42', '<' ) &&
			$dbConnection->getType() === 'mysql' &&
			method_exists( $dbConnection, 'listViews' )
		) {

			# bug 43571: cannot clone VIEWs under MySQL
			$views = $dbConnection->listViews(
				self::$MWDB_PREFIX,
				__METHOD__
			);

			$tables = array_diff( $tables, $views );
		}

		$tables = array_map( [ $this, 'unprefixTable' ], $tables );

		// Don't duplicate test tables from the previous failed run
		$tables = array_filter( $tables, [ $this, 'isNotUnittest' ] );

		// @see MediaWikiTestCase::listTables
		if ( $dbConnection->getType() === 'sqlite' ) {
			$tables = array_filter( $tables, [ $this, 'isNotSearchindex' ] );
		}

		return $tables;
	}

	private function setupDatabaseTables() {
		if ( $this->dbSetup ) {
			return true;
		}

		$this->cloneDatabaseTables();
		$this->store->setup( false );
		$this->createDummyPage();

		$this->dbSetup = true;
	}

	private function cloneDatabaseTables() {
		$tablesToBeCloned = $this->generateListOfTables();

		// MW's DatabaseSqlite does some magic on its own therefore
		// we force our way
		if ( $this->getDBConnection()->getType() === 'sqlite' ) {
			CloneDatabase::changePrefix( "" );

			$this->cloneDatabase = new CloneDatabase(
				$this->getDBConnection(),
				$tablesToBeCloned,
				""
			);
		} else {
			$this->cloneDatabase = new CloneDatabase(
				$this->getDBConnection(),
				$tablesToBeCloned,
				$this->getDBPrefix()
			);

			// Ensure no leftovers
			if ( $this->getDBConnection()->getType() === 'postgres' ) {
				$this->cloneDatabase->destroy( true );
			}

			// Rebuild the DB (in order to exclude temporary table usage)
			// otherwise some tests will fail with
			// "Error: 1137 Can't reopen table" on MySQL (see Issue #80)
			$this->cloneDatabase->useTemporaryTables( false );
			$this->cloneDatabase->cloneTableStructure();

			// #3197
			// @see https://github.com/wikimedia/mediawiki/commit/6badc7415684df54d6672098834359223b859507
			CloneDatabase::changePrefix( self::$UTDB_PREFIX );
		}
	}

	private function createDummyPage() {
		$pageCreator = new PageCreator();
		$pageCreator
			->createPage( MediaWikiServices::getInstance()->getTitleFactory()->newFromText( 'SMWUTDummyPage' ) )
			->doEdit( 'SMW dummy page' );
	}

	private function destroyDatabaseTables() {
		if ( !$this->cloneDatabase instanceof CloneDatabase ) {
			throw new RuntimeException( 'CloneDatabase instance is missing, unable to destory the database tables' );
		}

		$this->cloneDatabase->destroy( true );
		$this->availableDatabaseTypes = $this->supportedDatabaseTypes;
		$this->dbSetup = false;
	}

	private function rollbackOpenDatabaseTransactions() {
		if ( $this->getDBConnection() ) {

			while ( $this->getDBConnection()->trxLevel() > 0 ) {
				$this->getDBConnection()->rollback();
			}

			// 1.26 was mde protected
			// $this->getDBConnection()->ignoreErrors( false );
		}
	}

	private function unprefixTable( $tableName ) {
		return substr( $tableName, strlen( self::$MWDB_PREFIX ) );
	}

	private function isNotUnittest( $table ) {
		return strpos( $table, 'unittest_' ) === false && strpos( $table, 'sunittest_' ) === false;
	}

	private function isNotSearchindex( $table ) {
		return strpos( $table, 'searchindex' ) === false && strpos( $table, 'smw_ft_search' ) === false;
	}

	private function isAvailableDatabaseType() {
		return in_array( $this->getDBConnection()->getType(), $this->availableDatabaseTypes );
	}

}
