<?php

namespace XF\Install;

use XF\App;
use XF\Db\AbstractAdapter;
use XF\Db\AbstractStatement;
use XF\Db\Exception;
use XF\Db\Schema\Alter;
use XF\Db\Schema\Create;
use XF\Db\Schema\Drop;
use XF\Db\SchemaManager;
use XF\Entity\ActivitySummarySection;
use XF\Entity\Phrase;
use XF\Entity\Widget;
use XF\Finder\WidgetFinder;
use XF\Mvc\Entity\Structure;
use XF\PrintableException;
use XF\Repository\ForumTypeRepository;
use XF\Repository\IconRepository;
use XF\Repository\ThreadTypeRepository;
use XF\Util\Json;

use function count, in_array, intval, is_array, strlen;

/**
 * Class InstallHelperTrait
 *
 * The methods here are designed to be called by upgrade steps for various uses. This is a trait so it can be shared
 * with both the core software installer and add-on setup classes.
 *
 * @package XF\Install
 */
trait InstallHelperTrait
{
	/**
	 * @var App
	 */
	protected $app;

	/**
	 * Convert all SERIALIZED data controlled by the entity named in $entityName to JSON.
	 * Serialized columns are identified in $columns.
	 *
	 * @param       $entityName
	 * @param array $columns
	 * @param       $position
	 * @param array $stepData
	 * @param bool $singlePass
	 *
	 * @return array|bool
	 */
	public function entityColumnsToJson($entityName, array $columns, $position, array $stepData, $singlePass = false, $perPage = 1000)
	{
		$class = \XF::stringToClass($entityName, '%s\Entity\%s');

		$structure = $class::getStructure(new Structure());

		$primaryKeys = is_array($structure->primaryKey) ? $structure->primaryKey : [$structure->primaryKey];

		$regex = '^([abCdioOsS]:|N;$)';
		return $this->tableColumnsToJson($structure->table, $primaryKeys, $columns, $position, $stepData, $singlePass, $regex, $perPage);
	}

	/**
	 * Convert all SERIALIZED data in $tableName to JSON.
	 * Serialized fields are identified in $serializedFields.
	 *
	 * @param        $tableName
	 * @param array  $primaryKeys
	 * @param array  $serializedFields
	 * @param        $position
	 * @param array  $stepData
	 * @param bool $singlePass
	 * @param string $regex
	 *
	 * @return array|bool
	 */
	public function tableColumnsToJson(
		$tableName,
		array $primaryKeys,
		array $serializedFields,
		$position,
		array $stepData,
		$singlePass = false,
		$regex = '^([abCdioOsS]:|N;$)',
		$perPage = 1000
	)
	{
		if (!strlen($regex))
		{
			throw new \LogicException("Must provide a limiting regex");
		}

		$db = \XF::db();
		$db->beginTransaction();

		if (count($primaryKeys) == 1 && isset($stepData['max']))
		{
			// for simple cases, we can do a bit more
			$perPage *= 2;
		}

		if (!isset($stepData['max']))
		{
			$stepData['max'] = $db->fetchOne("SELECT MAX({$primaryKeys[0]}) FROM {$tableName}");

			// basic updates for empty array columns
			foreach ($serializedFields AS $column)
			{
				$db->update($tableName, [$column => '[]'], "{$column} = ?", ['a:0:{}']);
			}
		}

		$whereSql = [];
		foreach ($serializedFields AS $column)
		{
			$whereSql[] = "({$column} <> '' AND {$column} <> '[]' AND {$column} REGEXP '{$regex}')";
		}

		if ($singlePass)
		{
			$perPage = 999999999;
		}

		$results = $db->fetchAll("
			SELECT
				" . implode(", ", $primaryKeys) . ",
				" . implode(", ", $serializedFields) . "
			FROM {$tableName}
			WHERE {$primaryKeys[0]} > ?
			AND (" . implode(' OR ', $whereSql) . ")
			ORDER BY {$primaryKeys[0]}
			LIMIT {$perPage}
		", $position);

		if (!$results)
		{
			$db->commit();
			return true;
		}

		$next = $this->updateResultsToJson($results, $tableName, $primaryKeys, $serializedFields);

		if ($next && count($primaryKeys) > 1)
		{
			// With a multi-column key, we assume the first column is an integer and use that for ordering.
			// But we might not finish all rows with that first column value, so fetch the rest of the last
			// one that we saw and convert them all here. Otherwise, they won't be converted on the next go around.

			$results = $db->fetchAll("
				SELECT
					" . implode(", ", $primaryKeys) . ",
					" . implode(", ", $serializedFields) . "
				FROM {$tableName}
				WHERE {$primaryKeys[0]} = ?
				AND (" . implode(' OR ', $whereSql) . ")
			", $next);

			$this->updateResultsToJson($results, $tableName, $primaryKeys, $serializedFields);
		}

		$db->commit();

		return [
			$next,
			"{$next} / {$stepData['max']}",
			$stepData,
		];
	}

	protected function updateResultsToJson(array $results, $tableName, array $primaryKeys, array $serializedFields)
	{
		$db = \XF::db();
		$next = 0;

		foreach ($results AS $result)
		{
			$next = $result[$primaryKeys[0]];
			$newValues = [];

			foreach ($serializedFields AS $column)
			{
				$oldValue = $result[$column];
				if ($oldValue !== '')
				{
					$newValue = Json::decodeJsonOrSerialized($oldValue);
					if ($newValue !== false || $oldValue === 'b:0;')
					{
						$encodedValue = json_encode($newValue);
						if ($encodedValue === false && $newValue !== false)
						{
							$pk = $primaryKeys[0];
							$pkValue = $result[$pk];

							$errorMessage = "Error doing JSON conversion $tableName.$column ($pk=$pkValue): "
								. json_last_error_msg() . '. Forcing partial conversion.';

							if (intval($pkValue))
							{
								try
								{
									$db->insert('xf_json_convert_error', [
										'table_name' => substr($tableName, 0, 100),
										'column_name' => substr($column, 0, 100),
										'pk_id' => intval($pkValue),
										'original_value' => $oldValue,
									], false, 'original_value = VALUES(original_value)');

									$errorMessage .= ' (Original value logged into xf_json_convert_error table)';
								}
								catch (Exception $e)
								{
								}
							}

							\XF::logError($errorMessage, true);

							$encodedValue = json_encode($newValue, JSON_PARTIAL_OUTPUT_ON_ERROR);
						}

						$newValues[$column] = $encodedValue;
					}
				}
			}

			if (!empty($newValues))
			{
				$pkConditions = [];
				$pkValues = [];

				foreach ($primaryKeys AS $pk)
				{
					$pkConditions[] = "$pk = ?";
					$pkValues[] = $result[$pk];
				}

				$db->update($tableName, $newValues, implode(' AND ', $pkConditions), $pkValues);
			}
		}

		return $next;
	}

	/**
	 * @param $sql
	 * @param string|array $bind
	 * @param bool $suppressAll
	 *
	 * @return bool|AbstractStatement
	 * @throws Exception|\Exception
	 */
	public function executeUpgradeQuery($sql, $bind = [], $suppressAll = false)
	{
		try
		{
			return $this->db()->query($sql, $bind);
		}
		catch (Exception $e)
		{
			if ($suppressAll)
			{
				return false;
			}

			$message = $e->getMessage();
			if (preg_match('/(have an error in your SQL syntax|table \'.*\' doesn\'t exist|Unknown column|doesn\'t have a default value|Data truncated)/i', $message))
			{
				// we don't want to suppress errors in the query that should generally be corrected
				throw $e;
			}

			return false;
		}
	}

	public function migrateTableToReactions($tableName, $likesColumn = 'likes', $likeUsersColumn = 'like_users')
	{
		if (!$this->schemaManager()->columnExists($tableName, $likesColumn))
		{
			// likes column is gone, so assume this has been run
			return;
		}

		$this->alterTable($tableName, function (Alter $table) use ($likesColumn, $likeUsersColumn)
		{
			$table->changeColumn($likesColumn)->type('int')->unsigned(false)->renameTo('reaction_score');
			$table->addColumn('reactions', 'blob')->nullable()->after('reaction_score');
			$table->renameColumn($likeUsersColumn, 'reaction_users');
		});

		$this->executeUpgradeQuery('UPDATE `' . $tableName . '` SET reactions = CONCAT(\'{"1":\', reaction_score, \'}\') WHERE reaction_score > 0');
	}

	public function renameLikeAlertOptionsToReactions($contentTypes, $oldAction = 'like')
	{
		foreach ((array) $contentTypes AS $contentType)
		{
			$old = "{$contentType}_{$oldAction}";
			$new = "{$contentType}_reaction";

			$this->executeUpgradeQuery('UPDATE xf_user_alert_optout SET alert = ? WHERE alert = ?', [$new, $old]);
			$this->executeUpgradeQuery('UPDATE xf_user_push_optout SET push = ? WHERE push = ?', [$new, $old]);

			$this->executeUpgradeQuery('UPDATE xf_user_option SET alert_optout = REPLACE(alert_optout, ?, ?) WHERE alert_optout LIKE ?', [$old, $new, $this->db()->escapeLike($old, '%?%')]);
			$this->executeUpgradeQuery('UPDATE xf_user_option SET push_optout = REPLACE(push_optout, ?, ?) WHERE push_optout LIKE ?', [$old, $new, $this->db()->escapeLike($old, '%?%')]);
		}
	}

	public function renameLikeAlertsToReactions($contentTypes, $renameNewsFeed = true, $oldAction = 'like')
	{
		$db = $this->db();
		$contentTypesQuoted = $db->quote((array) $contentTypes);

		$this->executeUpgradeQuery("
			UPDATE xf_user_alert
			SET action = 'reaction',
				extra_data = '{\"reaction_id\":1}'
			WHERE content_type IN({$contentTypesQuoted})
			AND action = ?
		", $oldAction);

		if ($renameNewsFeed)
		{
			$this->executeUpgradeQuery("
				UPDATE xf_news_feed
				SET action = 'reaction',
					extra_data = '{\"reaction_id\":1}'
				WHERE content_type IN({$contentTypesQuoted})
				AND action = ?
			", $oldAction);
		}
	}

	public function renameLikeCriteriaToReactions($criteriaTable, $primaryKey, $criteriaField = 'user_criteria')
	{
		$db = $this->db();

		$items = $this->db()->fetchPairs("
			SELECT `$primaryKey`, `$criteriaField`
			FROM `$criteriaTable`
			WHERE `$criteriaField` LIKE '%\"like_count\"%'
				OR `$criteriaField` LIKE '%\"like_ratio\"%'
			ORDER BY `$primaryKey`
		");

		if (!$items)
		{
			return;
		}

		foreach ($items AS $key => $criteria)
		{
			$encodeF = 'json_encode';
			$rules = @json_decode($criteria, true);

			if (!$rules)
			{
				$encodeF = 'serialize';
				$rules = @unserialize($criteria);
			}

			if (!$rules)
			{
				continue;
			}

			foreach ($rules AS &$rule)
			{
				if ($rule['rule'] == 'like_count')
				{
					$rule['rule'] = 'reaction_score';
					$rule['data']['reactions'] = $rule['data']['likes'];
					unset($rule['data']['likes']);
				}
				else if ($rule['rule'] == 'like_ratio')
				{
					$rule['rule'] = 'reaction_ratio';
				}
			}

			$db->update($criteriaTable, [
				$criteriaField => $encodeF($rules),
			], "$primaryKey = ?", $key, 'IGNORE');
		}
	}

	public function renameLikePermissionsToReactions(array $permissionGroupIds, $likePermissionId = 'like', $reactPermissionId = 'react')
	{
		$globalPermissions = [];
		$contentPermissions = [];

		foreach ($permissionGroupIds AS $permissionGroupId => $contentPermission)
		{
			$globalPermissions[] = $permissionGroupId;
			if ($contentPermission)
			{
				$contentPermissions[] = $permissionGroupId;
			}
		}

		$db = $this->db();

		$globalPermissionsQuoted = $db->quote($globalPermissions);

		$this->executeUpgradeQuery("
			UPDATE xf_permission_entry
			SET permission_id = '{$reactPermissionId}'
			WHERE permission_id = ?
			AND permission_group_id IN({$globalPermissionsQuoted})
		", $likePermissionId);

		if ($contentPermissions)
		{
			$contentPermissionsQuoted = $db->quote($contentPermissions);

			$this->executeUpgradeQuery("
				UPDATE xf_permission_entry_content
				SET permission_id = 'react'
				WHERE permission_id = ?
				AND permission_group_id IN({$contentPermissionsQuoted})
			", $likePermissionId);
		}
	}

	public function renameLikeStatsToReactions($contentTypes)
	{
		if (!is_array($contentTypes))
		{
			$contentTypes = [$contentTypes];
		}

		foreach ($contentTypes AS $contentType)
		{
			$this->executeUpgradeQuery("
				UPDATE xf_stats_daily
				SET stats_type = ?
				WHERE stats_type = ?
			", ["{$contentType}_reaction", "{$contentType}_like"]);
		}
	}

	public function addPrefixDescHelpPhrases(array $types)
	{
		foreach ($types AS $contentType => $table)
		{
			$prefixIds = $this->db()->fetchAllColumn("SELECT prefix_id FROM `$table`");

			foreach ($prefixIds AS $prefixId)
			{
				$this->insertCustomMasterPhrase(
					sprintf('%s_prefix_desc.%d', $contentType, $prefixId),
					''
				);
				$this->insertCustomMasterPhrase(
					sprintf('%s_prefix_help.%d', $contentType, $prefixId),
					''
				);
			}
		}
	}

	public function insertCustomMasterPhrase(string $title, string $text)
	{
		/** @var Phrase $phrase */
		$phrase = $this->app->em()->create(Phrase::class);
		$phrase->title = $title;
		$phrase->phrase_text = $text;
		$phrase->language_id = 0;
		$phrase->addon_id = '';
		$phrase->save(false);
	}

	public function createWidget($widgetKey, $definitionId, array $config, $title = '')
	{
		/** @var Widget $widget */
		$widget = $this->app->em()->create(Widget::class);
		$widget->widget_key = $widgetKey;
		$widget->definition_id = $definitionId;
		$widget->bulkSet($config);
		$success = $widget->save(false);

		if ($success)
		{
			$masterTitle = $widget->getMasterPhrase();
			$masterTitle->phrase_text = $title;
			$masterTitle->save(false);
		}
	}

	/**
	 * Allows widget positions to be edited, appended or removed using
	 * $positions [position => order]
	 * To add a new position, simply add it to the $newPositions array
	 * with an appropriate order. To edit an existing position, do the same.
	 * To remove a position, set the order for that position to null.
	 *
	 * @param       $widgetKey
	 * @param array $newPositions
	 *
	 * @return void
	 * @throws PrintableException
	 */
	public function editWidgetPositions($widgetKey, array $newPositions)
	{
		/** @var Widget $widget */
		$widget = $this->app()->finder(WidgetFinder::class)
			->where('widget_key', $widgetKey)
			->fetchOne();

		if ($widget)
		{
			$positions = $widget->positions;

			foreach ($newPositions AS $position => $order)
			{
				if ($order === null)
				{
					unset($positions[$position]);
				}
				else
				{
					$positions[$position] = $order;
				}
			}

			$widget->set('positions', $positions);
			$widget->save(false);
		}
	}

	public function createActivitySummarySection($definitionId, array $config, $title = '')
	{
		/** @var ActivitySummarySection $section */
		$section = $this->app->em()->create(ActivitySummarySection::class);
		$section->definition_id = $definitionId;
		$section->bulkSet($config);
		$success = $section->save(false);

		if ($success)
		{
			$masterTitle = $section->getMasterPhrase();
			$masterTitle->phrase_text = $title;
			$masterTitle->save(false);
		}
	}

	public function deleteWidget($widgetKey)
	{
		$widget = $this->app->finder(WidgetFinder::class)->where('widget_key', $widgetKey)->fetchOne();
		if (!$widget)
		{
			return;
		}
		$widget->delete(false);
	}

	public function applyGlobalPermission($applyGroupId, $applyPermissionId, $dependGroupId = null, $dependPermissionId = null)
	{
		$db = $this->db();

		if ($dependGroupId && $dependPermissionId)
		{
			$db->query("
				REPLACE INTO xf_permission_entry
					(user_group_id, user_id, permission_group_id, permission_id, permission_value, permission_value_int)
				SELECT user_group_id, user_id, ?, ?, 'allow', 0
				FROM xf_permission_entry
				WHERE permission_group_id = ?
					AND permission_id = ?
					AND permission_value = 'allow'
			", [$applyGroupId, $applyPermissionId, $dependGroupId, $dependPermissionId]);
		}
		else
		{
			$db->query("
				REPLACE INTO xf_permission_entry
					(user_group_id, user_id, permission_group_id, permission_id, permission_value, permission_value_int)
				SELECT DISTINCT user_group_id, user_id, ?, ?, 'allow', 0
				FROM xf_permission_entry
			", [$applyGroupId, $applyPermissionId]);
		}
	}

	public function applyGlobalPermissionInt($applyGroupId, $applyPermissionId, $applyValue, $dependGroupId = null, $dependPermissionId = null)
	{
		$db = $this->db();

		if ($dependGroupId && $dependPermissionId)
		{
			$db->query("
				REPLACE INTO xf_permission_entry
					(user_group_id, user_id, permission_group_id, permission_id, permission_value, permission_value_int)
				SELECT user_group_id, user_id, ?, ?, 'use_int', ?
				FROM xf_permission_entry
				WHERE permission_group_id = ?
					AND permission_id = ?
					AND permission_value = 'allow'
			", [$applyGroupId, $applyPermissionId, $applyValue, $dependGroupId, $dependPermissionId]);
		}
		else
		{
			$db->query("
				REPLACE INTO xf_permission_entry
					(user_group_id, user_id, permission_group_id, permission_id, permission_value, permission_value_int)
				SELECT DISTINCT user_group_id, user_id, ?, ?, 'use_int', ?
				FROM xf_permission_entry
			", [$applyGroupId, $applyPermissionId, $applyValue]);
		}
	}

	public function applyContentPermission($applyGroupId, $applyPermissionId, $dependGroupId, $dependPermissionId)
	{
		$db = $this->db();

		$db->query("
			REPLACE INTO xf_permission_entry_content
				(content_type, content_id, user_group_id, user_id,
				permission_group_id, permission_id, permission_value, permission_value_int)
			SELECT content_type, content_id, user_group_id, user_id, ?, ?, 'content_allow', 0
			FROM xf_permission_entry_content
			WHERE permission_group_id = ?
				AND permission_id = ?
				AND permission_value = 'content_allow'
		", [$applyGroupId, $applyPermissionId, $dependGroupId, $dependPermissionId]);
	}

	public function applyAdminPermission($applyPermissionId, $dependPermissionId)
	{
		// note: this doesn't rebuild the admin permission cache -- that should happen when the permission
		// is inserted, so this is only safe to use on its own when inserting the permission
		$this->db()->query("
			INSERT IGNORE INTO xf_admin_permission_entry
				(user_id, admin_permission_id)
			SELECT user_id, ?
			FROM xf_admin_permission_entry
			WHERE admin_permission_id = ?
		", [$applyPermissionId, $dependPermissionId]);
	}

	public function uninstallContentTypeData($contentTypes)
	{
		if (!is_array($contentTypes))
		{
			$contentTypes = [$contentTypes];
		}

		$db = $this->db();

		$contentTypesQuoted = $db->quote($contentTypes);

		$db->beginTransaction();

		$contentTypeTables = [
			'xf_approval_queue',
			'xf_bookmark_item',
			'xf_change_log',
			'xf_content_spam_cache',
			'xf_deletion_log',
			'xf_edit_history',
			'xf_moderator_content',
			'xf_moderator_log',
			'xf_news_feed',
			'xf_poll',
			'xf_reaction_content',
			'xf_report',
			'xf_spam_trigger_log',
			'xf_tag_content',
			'xf_user_alert',
		];
		foreach ($contentTypeTables AS $table)
		{
			$db->delete($table, 'content_type IN (' . $contentTypesQuoted . ')');
		}

		// TODO: should try to remove report comments

		// let these be cleaned up over time
		$db->update('xf_attachment', ['unassociated' => 1, 'content_id' => 0], 'content_type IN (' . $contentTypesQuoted . ')');

		$db->commit();
	}

	/**
	 * @param string[] $primaryKeys
	 * @param string[] $rules
	 * @param mixed[] $stepData
	 *
	 * @return mixed[]|bool
	 */
	public function removeCriteriaRules(
		string $tableName,
		array $primaryKeys,
		string $criteriaField,
		array $rules,
		int $position,
		array $stepData,
		int $perPage = 1000
	)
	{
		$this->db()->beginTransaction();

		if (count($primaryKeys) == 1 && isset($stepData['max']))
		{
			// for simple cases, we can do a bit more
			$perPage *= 2;
		}

		if (!isset($stepData['max']))
		{
			$stepData['max'] = $this->db()->fetchOne(
				"SELECT MAX({$primaryKeys[0]}) FROM {$tableName}"
			);
		}

		$results = $this->db()->fetchAll(
			"SELECT
				" . implode(', ', $primaryKeys) . ",
				{$criteriaField}
				FROM {$tableName}
				WHERE {$primaryKeys[0]} > ?
					AND {$criteriaField} <> ''
					AND {$criteriaField} <> '[]'
				ORDER BY {$primaryKeys[0]}
				LIMIT {$perPage}",
			[$position]
		);
		if (!$results)
		{
			$this->db()->commit();
			return true;
		}

		$next = $this->removeCriteriaRulesFromResults(
			$results,
			$tableName,
			$primaryKeys,
			$criteriaField,
			$rules
		);
		if ($next && count($primaryKeys) > 1)
		{
			// with a multi-column key, we assume the first column is an integer
			// and use that for ordering, but we might not finish all rows with
			// that first column value so fetch the rest of the last one that we
			// saw and convert them all here, otherwise they won't be converted
			// on the next go around
			$results = $this->db()->fetchAll(
				"SELECT
					" . implode(', ', $primaryKeys) . ",
					{$criteriaField}
					FROM {$tableName}
					WHERE {$primaryKeys[0]} = ?
						AND {$criteriaField} <> ''
						AND {$criteriaField} <> '[]'",
				[$next]
			);
			$this->removeCriteriaRulesFromResults(
				$results,
				$tableName,
				$primaryKeys,
				$criteriaField,
				$rules
			);
		}

		$this->db()->commit();

		return [$next, "{$next} / {$stepData['max']}", $stepData];
	}

	/**
	 * @param string[] $primaryKeys
	 * @param string[] $rules
	 */
	protected function removeCriteriaRulesFromResults(
		array $results,
		string $tableName,
		array $primaryKeys,
		string $criteriaField,
		array $rules
	): int
	{
		$next = 0;

		foreach ($results AS $result)
		{
			$next = $result[$primaryKeys[0]];

			$criteria = @json_decode($result[$criteriaField], true);
			if (!$criteria)
			{
				continue;
			}

			$changed = false;
			foreach ($criteria AS $index => $criterion)
			{
				if (in_array($criterion['rule'], $rules, true))
				{
					unset($criteria[$index]);
					$changed = true;
				}
			}

			if (!$changed)
			{
				continue;
			}

			$pkConditions = [];
			$pkValues = [];
			foreach ($primaryKeys AS $pk)
			{
				$pkConditions[] = "$pk = ?";
				$pkValues[] = $result[$pk];
			}

			$this->db()->update(
				$tableName,
				[$criteriaField => json_encode(
					array_values($criteria),
					JSON_PARTIAL_OUTPUT_ON_ERROR
				)],
				implode(' AND ', $pkConditions),
				$pkValues
			);
		}

		return $next;
	}

	public function insertForumType($type, $handlerClass, $addOnId, $rebuildCache = true)
	{
		$this->db()->insert('xf_forum_type', [
			'forum_type_id' => $type,
			'handler_class' => $handlerClass,
			'addon_id' => $addOnId,
		], false, 'handler_class = VALUES(handler_class), addon_id = VALUES(addon_id)');

		if ($rebuildCache)
		{
			$iconRepo = $this->app->repository(IconRepository::class);
			$iconRepo->enqueueUsageAnalyzer('forum_type');

			\XF::runOnce('rebuildForumTypeCache', function ()
			{
				$this->app->repository(ForumTypeRepository::class)->rebuildForumTypeCache();
			});
		}
	}

	public function insertThreadType($type, $handlerClass, $addOnId, $rebuildCache = true)
	{
		$this->db()->insert('xf_thread_type', [
			'thread_type_id' => $type,
			'handler_class' => $handlerClass,
			'addon_id' => $addOnId,
		], false, 'handler_class = VALUES(handler_class), addon_id = VALUES(addon_id)');

		if ($rebuildCache)
		{
			$iconRepo = $this->app->repository(IconRepository::class);
			$iconRepo->enqueueUsageAnalyzer('thread_type');

			\XF::runOnce('rebuildThreadTypeCache', function ()
			{
				$this->app->repository(ThreadTypeRepository::class)->rebuildThreadTypeCache();
			});
		}
	}

	/**
	 * @return App
	 */
	protected function app()
	{
		return $this->app;
	}

	/**
	 * @return AbstractAdapter
	 */
	protected function db()
	{
		return $this->app->db();
	}

	/**
	 * @return SchemaManager
	 */
	protected function schemaManager()
	{
		return $this->db()->getSchemaManager();
	}

	/**
	 * @param string $tableName
	 * @param \Closure(Alter): void $toApply
	 */
	protected function alterTable($tableName, \Closure $toApply)
	{
		$this->schemaManager()->alterTable($tableName, $toApply);
	}

	protected function renameTable($oldTableName, $newTableName)
	{
		$this->schemaManager()->renameTable($oldTableName, $newTableName);
	}

	/**
	 * @param $tableName
	 * @param \Closure(Create): void $toApply
	 */
	protected function createTable($tableName, \Closure $toApply)
	{
		$this->schemaManager()->createTable($tableName, $toApply);
	}

	/**
	 * @param $tableName
	 * @param (\Closure(Drop): void)|null $toApply
	 */
	protected function dropTable($tableName, ?\Closure $toApply = null)
	{
		$this->schemaManager()->dropTable($tableName, $toApply);
	}

	public function tableExists($tableName)
	{
		return $this->schemaManager()->tableExists($tableName);
	}

	public function columnExists($tableName, $column, &$definition = null)
	{
		return $this->schemaManager()->columnExists($tableName, $column, $definition);
	}
}
